From aaadf16b1b39ec7c742a5183ddc2cc4e2af34bab Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Tue, 9 Dec 2025 19:08:03 -0800 Subject: [PATCH 01/36] [ty] bump dependencies to pull in Salsa support for `ordermap` (#21854) --- Cargo.lock | 32 +++++++++++++++------------- Cargo.toml | 6 +++--- crates/ty_python_semantic/Cargo.toml | 4 ++-- fuzz/Cargo.toml | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bde255074..d0018e76e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,7 +1016,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1238,9 +1238,9 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd" +checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a" dependencies = [ "attribute-derive", "quote", @@ -1249,14 +1249,15 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af" +checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb" dependencies = [ "compact_str", "get-size-derive2", "hashbrown 0.16.1", "indexmap", + "ordermap", "smallvec", ] @@ -1763,7 +1764,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2233,9 +2234,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordermap" -version = "0.5.12" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af" +checksum = "ed637741ced8fb240855d22a2b4f208dab7a06bcce73380162e5253000c16758" dependencies = [ "indexmap", "serde", @@ -3571,7 +3572,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3589,7 +3590,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1" dependencies = [ "boxcar", "compact_str", @@ -3600,6 +3601,7 @@ dependencies = [ "indexmap", "intrusive-collections", "inventory", + "ordermap", "parking_lot", "portable-atomic", "rustc-hash", @@ -3613,12 +3615,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1" [[package]] name = "salsa-macros" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1" dependencies = [ "proc-macro2", "quote", @@ -3972,7 +3974,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -5026,7 +5028,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 554ad219d2..0badc79e3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ etcetera = { version = "0.11.0" } fern = { version = "0.7.0" } filetime = { version = "0.2.23" } getrandom = { version = "0.3.1" } -get-size2 = { version = "0.7.0", features = [ +get-size2 = { version = "0.7.3", features = [ "derive", "smallvec", "hashbrown", @@ -129,7 +129,7 @@ memchr = { version = "2.7.1" } mimalloc = { version = "0.1.39" } natord = { version = "1.0.9" } notify = { version = "8.0.0" } -ordermap = { version = "0.5.0" } +ordermap = { version = "1.0.0" } path-absolutize = { version = "3.1.1" } path-slash = { version = "0.2.1" } pathdiff = { version = "0.2.1" } @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index 140eec33be..957126cf3a 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -33,11 +33,11 @@ camino = { workspace = true } colored = { workspace = true } compact_str = { workspace = true } drop_bomb = { workspace = true } -get-size2 = { workspace = true, features = ["indexmap"]} +get-size2 = { workspace = true, features = ["indexmap", "ordermap"]} indexmap = { workspace = true } itertools = { workspace = true } ordermap = { workspace = true } -salsa = { workspace = true, features = ["compact_str"] } +salsa = { workspace = true, features = ["compact_str", "ordermap"] } thiserror = { workspace = true } tracing = { workspace = true } rustc-hash = { workspace = true } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index adbdcd7545..66041ec1d3 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" } ty_vendored = { path = "../crates/ty_vendored" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", From 8293afe2ae7af79fe7d646c39dc2636d38b29fa0 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 10 Dec 2025 12:39:31 +0530 Subject: [PATCH 02/36] Remove hack about unknown options warning (#21887) This hack was introduced to reduce the amount of warnings that users would get while transitioning to the new settings format (https://github.com/astral-sh/ruff/pull/19787) but now that we're near the beta release, it would be good to remove this. --- crates/ty_server/src/server.rs | 26 +----------- crates/ty_server/src/session.rs | 51 +++++++++++++----------- crates/ty_server/tests/e2e/initialize.rs | 4 +- 3 files changed, 32 insertions(+), 49 deletions(-) diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 321a74857e..402800d325 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -3,7 +3,7 @@ use self::schedule::spawn_main_loop; use crate::PositionEncoding; use crate::capabilities::{ResolvedClientCapabilities, server_capabilities}; -use crate::session::{InitializationOptions, Session}; +use crate::session::{InitializationOptions, Session, warn_about_unknown_options}; use anyhow::Context; use lsp_server::Connection; use lsp_types::{ClientCapabilities, InitializeParams, MessageType, Url}; @@ -96,29 +96,7 @@ impl Server { let unknown_options = &initialization_options.options.unknown; if !unknown_options.is_empty() { - // HACK: Old versions of the ty VS Code extension used a custom schema for settings - // which was changed in version 2025.35.0. This is to ensure that users don't receive - // unnecessary warnings when using an older version of the extension. This should be - // removed after a few releases. - if !unknown_options.contains_key("settings") - || !unknown_options.contains_key("globalSettings") - { - tracing::warn!( - "Received unknown options during initialization: {}", - serde_json::to_string_pretty(&unknown_options) - .unwrap_or_else(|_| format!("{unknown_options:?}")) - ); - - client.show_warning_message(format_args!( - "Received unknown options during initialization: '{}'. \ - Refer to the logs for more details", - unknown_options - .keys() - .map(String::as_str) - .collect::>() - .join("', '") - )); - } + warn_about_unknown_options(&client, None, unknown_options); } // Get workspace URLs without settings - settings will come from workspace/configuration diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index d97e11ac48..233b0e6aa3 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -1,6 +1,6 @@ //! Data model, state management, and configuration resolution. -use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::ops::{Deref, DerefMut}; use std::panic::RefUnwindSafe; use std::sync::Arc; @@ -467,28 +467,7 @@ impl Session { let unknown_options = &options.unknown; if !unknown_options.is_empty() { - // HACK: This is to ensure that users with an older version of the ty VS Code - // extension don't get warnings about unknown options when they are using a newer - // version of the language server. This should be removed after a few releases. - if !unknown_options.contains_key("importStrategy") - && !unknown_options.contains_key("interpreter") - { - tracing::warn!( - "Received unknown options for workspace `{url}`: {}", - serde_json::to_string_pretty(unknown_options) - .unwrap_or_else(|_| format!("{unknown_options:?}")) - ); - - client.show_warning_message(format!( - "Received unknown options for workspace `{url}`: '{}'. \ - Refer to the logs for more details.", - unknown_options - .keys() - .map(String::as_str) - .collect::>() - .join("', '") - )); - } + warn_about_unknown_options(client, Some(&url), unknown_options); } combined_global_options.combine_with(Some(global)); @@ -1595,3 +1574,29 @@ impl DocumentHandle { Ok(requires_clear_diagnostics) } } + +/// Warns about unknown options received by the server. +/// +/// If `workspace_url` is `Some`, it indicates that the unknown options were received during a +/// workspace initialization, otherwise they were received during the server initialization. +pub(super) fn warn_about_unknown_options( + client: &Client, + workspace_url: Option<&Url>, + unknown_options: &HashMap, +) { + let message = if let Some(workspace_url) = workspace_url { + format!( + "Received unknown options for workspace `{workspace_url}`: {}", + serde_json::to_string_pretty(unknown_options) + .unwrap_or_else(|_| format!("{unknown_options:?}")) + ) + } else { + format!( + "Received unknown options during initialization: {}", + serde_json::to_string_pretty(unknown_options) + .unwrap_or_else(|_| format!("{unknown_options:?}")) + ) + }; + tracing::warn!("{message}"); + client.show_warning_message(message); +} diff --git a/crates/ty_server/tests/e2e/initialize.rs b/crates/ty_server/tests/e2e/initialize.rs index 1526e78022..8611afdccd 100644 --- a/crates/ty_server/tests/e2e/initialize.rs +++ b/crates/ty_server/tests/e2e/initialize.rs @@ -402,7 +402,7 @@ fn unknown_initialization_options() -> Result<()> { insta::assert_json_snapshot!(show_message_params, @r#" { "type": 2, - "message": "Received unknown options during initialization: 'bar'. Refer to the logs for more details" + "message": "Received unknown options during initialization: {\n /"bar/": null\n}" } "#); @@ -427,7 +427,7 @@ fn unknown_options_in_workspace_configuration() -> Result<()> { insta::assert_json_snapshot!(show_message_params, @r#" { "type": 2, - "message": "Received unknown options for workspace `file:///foo`: 'bar'. Refer to the logs for more details." + "message": "Received unknown options for workspace `file:///foo`: {\n /"bar/": null\n}" } "#); From d2aabeaaa2563991ad64fb57a688ee2d03390e63 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Dec 2025 04:12:18 -0500 Subject: [PATCH 03/36] [ty] Respect `kw_only` from parent class (#21820) ## Summary Closes https://github.com/astral-sh/ty/issues/1769. --------- Co-authored-by: Carl Meyer --- .../mdtest/dataclasses/dataclasses.md | 105 ++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 32 +++++- 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index fa6f76de75..7c64175658 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -701,6 +701,111 @@ Employee("Alice", e_id=1) Employee("Alice", 1) # error: [too-many-positional-arguments] ``` +### Inherited fields with class-level `kw_only` + +When a child dataclass uses `@dataclass(kw_only=True)`, the `kw_only` setting should only apply to +fields defined in the child class, not to inherited fields from parent classes. + +This is a regression test for . + +```toml +[environment] +python-version = "3.10" +``` + +```py +from dataclasses import dataclass + +@dataclass +class Inner: + inner: int + +@dataclass(kw_only=True) +class Outer(Inner): + outer: int + +# Inherited field `inner` is positional, new field `outer` is keyword-only +reveal_type(Outer.__init__) # revealed: (self: Outer, inner: int, *, outer: int) -> None + +Outer(0, outer=5) # OK +Outer(inner=0, outer=5) # Also OK +# error: [missing-argument] +# error: [too-many-positional-arguments] +Outer(0, 5) +``` + +This also works when the parent class uses the `KW_ONLY` sentinel: + +```py +from dataclasses import dataclass, KW_ONLY + +@dataclass +class Parent: + a: int + _: KW_ONLY + b: str + +@dataclass(kw_only=True) +class Child(Parent): + c: bytes + +# `a` is positional (from parent), `b` is keyword-only (from parent's KW_ONLY), +# `c` is keyword-only (from child's kw_only=True) +reveal_type(Child.__init__) # revealed: (self: Child, a: int, *, b: str, c: bytes) -> None + +Child(1, b="hello", c=b"world") # OK +# error: [missing-argument] "No arguments provided for required parameters `b`, `c`" +# error: [too-many-positional-arguments] +Child(1, "hello", b"world") +``` + +And when the child class uses the `KW_ONLY` sentinel while inheriting from a parent: + +```py +from dataclasses import dataclass, KW_ONLY + +@dataclass +class Base: + x: int + +@dataclass +class Derived(Base): + y: str + _: KW_ONLY + z: bytes + +# `x` and `y` are positional, `z` is keyword-only (from Derived's KW_ONLY) +reveal_type(Derived.__init__) # revealed: (self: Derived, x: int, y: str, *, z: bytes) -> None + +Derived(1, "hello", z=b"world") # OK +# error: [missing-argument] +# error: [too-many-positional-arguments] +Derived(1, "hello", b"world") +``` + +The reverse case also works: when a parent has `kw_only=True` but the child doesn't, the parent's +fields stay keyword-only while the child's fields are positional: + +```py +from dataclasses import dataclass + +@dataclass(kw_only=True) +class KwOnlyParent: + parent_field: int + +@dataclass +class PositionalChild(KwOnlyParent): + child_field: str + +# `child_field` is positional (child's default), `parent_field` stays keyword-only +reveal_type(PositionalChild.__init__) # revealed: (self: PositionalChild, child_field: str, *, parent_field: int) -> None + +PositionalChild("hello", parent_field=1) # OK +# error: [missing-argument] +# error: [too-many-positional-arguments] +PositionalChild("hello", 1) +``` + ### `slots` If a dataclass is defined with `slots=True`, the `__slots__` attribute is generated as a tuple. It diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 855e8922a0..a5122431dd 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -21,7 +21,9 @@ use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD}; use crate::types::enums::enum_metadata; -use crate::types::function::{DataclassTransformerParams, KnownFunction}; +use crate::types::function::{ + DataclassTransformerFlags, DataclassTransformerParams, KnownFunction, +}; use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_generic_context, walk_specialization, }; @@ -2347,6 +2349,8 @@ impl<'db> ClassLiteral<'db> { }; // Dataclass transformer flags can be overwritten using class arguments. + // TODO this should be done more generally, not just in `own_synthesized_member`, so that + // `dataclass_params` always reflects the transformer params. if let Some(transformer_params) = transformer_params.as_mut() { if let Some(class_def) = self.definition(db).kind(db).as_class() { let module = parsed_module(db, self.file(db)).load(db); @@ -2376,6 +2380,8 @@ impl<'db> ClassLiteral<'db> { let has_dataclass_param = |param| { dataclass_params.is_some_and(|params| params.flags(db).contains(param)) + // TODO if we were correctly initializing `dataclass_params` from the + // transformer params, this fallback shouldn't be needed here. || transformer_params.is_some_and(|params| params.flags(db).contains(param)) }; @@ -2455,8 +2461,7 @@ impl<'db> ClassLiteral<'db> { } } - let is_kw_only = name == "__replace__" - || kw_only.unwrap_or(has_dataclass_param(DataclassFlags::KW_ONLY)); + let is_kw_only = name == "__replace__" || kw_only.unwrap_or(false); // Use the alias name if provided, otherwise use the field name let parameter_name = @@ -3175,6 +3180,27 @@ impl<'db> ClassLiteral<'db> { } } + // Resolve the kw_only to the class-level default. This ensures that when fields + // are inherited by child classes, they use their defining class's kw_only default. + if let FieldKind::Dataclass { + kw_only: ref mut kw @ None, + .. + } = field.kind + { + let class_kw_only_default = self + .dataclass_params(db) + .is_some_and(|params| params.flags(db).contains(DataclassFlags::KW_ONLY)) + // TODO this next part should not be necessary, if we were properly + // initializing `dataclass_params` from the dataclass-transform params, for + // metaclass and base-class-based dataclass-transformers. + || matches!( + field_policy, + CodeGeneratorKind::DataclassLike(Some(transformer_params)) + if transformer_params.flags(db).contains(DataclassTransformerFlags::KW_ONLY_DEFAULT) + ); + *kw = Some(class_kw_only_default); + } + attributes.insert(symbol.name().clone(), field); } } From ff7086d9ad24729891754ba597ca5abc513e4858 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 10 Dec 2025 04:31:28 -0500 Subject: [PATCH 04/36] [ty] Infer type of implicit `cls` parameter in method bodies (#21685) ## Summary Extends https://github.com/astral-sh/ruff/pull/20922 to infer unannotated `cls` parameters as `type[Self]` in method bodies. Part of https://github.com/astral-sh/ty/issues/159. --- crates/ruff_benchmark/benches/ty_walltime.rs | 2 +- .../resources/mdtest/annotations/self.md | 9 +- .../resources/mdtest/class/super.md | 3 +- ...licit_Super_Objec…_(f9e5e48e3a4a4c12).snap | 271 +++++++++--------- crates/ty_python_semantic/src/types.rs | 4 +- .../ty_python_semantic/src/types/generics.rs | 3 +- .../src/types/infer/builder.rs | 28 +- .../src/types/signatures.rs | 1 + 8 files changed, 167 insertions(+), 154 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 9f29b193a5..7a0f9a5848 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 13000, + 13030, ); static TANJUN: Benchmark = Benchmark::new( diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 57690b3dc3..7fb465fdbd 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -63,6 +63,12 @@ python-version = "3.12" from typing import Self class A: + def __init__(self): + reveal_type(self) # revealed: Self@__init__ + + def __init_subclass__(cls, default_name, **kwargs): + reveal_type(cls) # revealed: type[Self@__init_subclass__] + def implicit_self(self) -> Self: reveal_type(self) # revealed: Self@implicit_self @@ -91,8 +97,7 @@ class A: @classmethod def a_classmethod(cls) -> Self: - # TODO: This should be type[Self@bar] - reveal_type(cls) # revealed: Unknown + reveal_type(cls) # revealed: type[Self@a_classmethod] return cls() @staticmethod diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index 750f589125..4c3d09560f 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -174,8 +174,7 @@ class B(A): @classmethod def f(cls): - # TODO: Once `cls` is supported, this should be `, >` - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , > super().f() super(B, B(42)).__init__(42) diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap index 75eefd748b..8a51223e6d 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap @@ -27,135 +27,134 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md 13 | 14 | @classmethod 15 | def f(cls): - 16 | # TODO: Once `cls` is supported, this should be `, >` - 17 | reveal_type(super()) # revealed: , Unknown> - 18 | super().f() - 19 | - 20 | super(B, B(42)).__init__(42) - 21 | super(B, B).f() - 22 | import enum - 23 | from typing import Any, Self, Never, Protocol, Callable - 24 | from ty_extensions import Intersection - 25 | - 26 | class BuilderMeta(type): - 27 | def __new__( - 28 | cls: type[Any], - 29 | name: str, - 30 | bases: tuple[type, ...], - 31 | dct: dict[str, Any], - 32 | ) -> BuilderMeta: - 33 | # revealed: , Any> - 34 | s = reveal_type(super()) - 35 | # revealed: Any - 36 | return reveal_type(s.__new__(cls, name, bases, dct)) - 37 | - 38 | class BuilderMeta2(type): - 39 | def __new__( - 40 | cls: type[BuilderMeta2], - 41 | name: str, - 42 | bases: tuple[type, ...], - 43 | dct: dict[str, Any], - 44 | ) -> BuilderMeta2: - 45 | # revealed: , > - 46 | s = reveal_type(super()) - 47 | return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2 - 48 | - 49 | class Foo[T]: - 50 | x: T - 51 | - 52 | def method(self: Any): - 53 | reveal_type(super()) # revealed: , Any> - 54 | - 55 | if isinstance(self, Foo): - 56 | reveal_type(super()) # revealed: , Any> - 57 | - 58 | def method2(self: Foo[T]): - 59 | # revealed: , Foo[T@Foo]> - 60 | reveal_type(super()) - 61 | - 62 | def method3(self: Foo): - 63 | # revealed: , Foo[Unknown]> - 64 | reveal_type(super()) - 65 | - 66 | def method4(self: Self): - 67 | # revealed: , Foo[T@Foo]> - 68 | reveal_type(super()) - 69 | - 70 | def method5[S: Foo[int]](self: S, other: S) -> S: - 71 | # revealed: , Foo[int]> - 72 | reveal_type(super()) - 73 | return self - 74 | - 75 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: - 76 | # revealed: , Foo[int]> | , Foo[str]> - 77 | reveal_type(super()) - 78 | return self - 79 | - 80 | def method7[S](self: S, other: S) -> S: - 81 | # error: [invalid-super-argument] - 82 | # revealed: Unknown - 83 | reveal_type(super()) - 84 | return self - 85 | - 86 | def method8[S: int](self: S, other: S) -> S: - 87 | # error: [invalid-super-argument] - 88 | # revealed: Unknown - 89 | reveal_type(super()) - 90 | return self - 91 | - 92 | def method9[S: (int, str)](self: S, other: S) -> S: - 93 | # error: [invalid-super-argument] - 94 | # revealed: Unknown - 95 | reveal_type(super()) - 96 | return self - 97 | - 98 | def method10[S: Callable[..., str]](self: S, other: S) -> S: - 99 | # error: [invalid-super-argument] -100 | # revealed: Unknown -101 | reveal_type(super()) -102 | return self -103 | -104 | type Alias = Bar -105 | -106 | class Bar: -107 | def method(self: Alias): -108 | # revealed: , Bar> -109 | reveal_type(super()) -110 | -111 | def pls_dont_call_me(self: Never): -112 | # revealed: , Unknown> -113 | reveal_type(super()) -114 | -115 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): -116 | # revealed: , Bar> -117 | reveal_type(super()) -118 | -119 | class P(Protocol): -120 | def method(self: P): -121 | # revealed: , P> -122 | reveal_type(super()) -123 | -124 | class E(enum.Enum): -125 | X = 1 -126 | -127 | def method(self: E): -128 | match self: -129 | case E.X: -130 | # revealed: , E> -131 | reveal_type(super()) + 16 | reveal_type(super()) # revealed: , > + 17 | super().f() + 18 | + 19 | super(B, B(42)).__init__(42) + 20 | super(B, B).f() + 21 | import enum + 22 | from typing import Any, Self, Never, Protocol, Callable + 23 | from ty_extensions import Intersection + 24 | + 25 | class BuilderMeta(type): + 26 | def __new__( + 27 | cls: type[Any], + 28 | name: str, + 29 | bases: tuple[type, ...], + 30 | dct: dict[str, Any], + 31 | ) -> BuilderMeta: + 32 | # revealed: , Any> + 33 | s = reveal_type(super()) + 34 | # revealed: Any + 35 | return reveal_type(s.__new__(cls, name, bases, dct)) + 36 | + 37 | class BuilderMeta2(type): + 38 | def __new__( + 39 | cls: type[BuilderMeta2], + 40 | name: str, + 41 | bases: tuple[type, ...], + 42 | dct: dict[str, Any], + 43 | ) -> BuilderMeta2: + 44 | # revealed: , > + 45 | s = reveal_type(super()) + 46 | return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2 + 47 | + 48 | class Foo[T]: + 49 | x: T + 50 | + 51 | def method(self: Any): + 52 | reveal_type(super()) # revealed: , Any> + 53 | + 54 | if isinstance(self, Foo): + 55 | reveal_type(super()) # revealed: , Any> + 56 | + 57 | def method2(self: Foo[T]): + 58 | # revealed: , Foo[T@Foo]> + 59 | reveal_type(super()) + 60 | + 61 | def method3(self: Foo): + 62 | # revealed: , Foo[Unknown]> + 63 | reveal_type(super()) + 64 | + 65 | def method4(self: Self): + 66 | # revealed: , Foo[T@Foo]> + 67 | reveal_type(super()) + 68 | + 69 | def method5[S: Foo[int]](self: S, other: S) -> S: + 70 | # revealed: , Foo[int]> + 71 | reveal_type(super()) + 72 | return self + 73 | + 74 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: + 75 | # revealed: , Foo[int]> | , Foo[str]> + 76 | reveal_type(super()) + 77 | return self + 78 | + 79 | def method7[S](self: S, other: S) -> S: + 80 | # error: [invalid-super-argument] + 81 | # revealed: Unknown + 82 | reveal_type(super()) + 83 | return self + 84 | + 85 | def method8[S: int](self: S, other: S) -> S: + 86 | # error: [invalid-super-argument] + 87 | # revealed: Unknown + 88 | reveal_type(super()) + 89 | return self + 90 | + 91 | def method9[S: (int, str)](self: S, other: S) -> S: + 92 | # error: [invalid-super-argument] + 93 | # revealed: Unknown + 94 | reveal_type(super()) + 95 | return self + 96 | + 97 | def method10[S: Callable[..., str]](self: S, other: S) -> S: + 98 | # error: [invalid-super-argument] + 99 | # revealed: Unknown +100 | reveal_type(super()) +101 | return self +102 | +103 | type Alias = Bar +104 | +105 | class Bar: +106 | def method(self: Alias): +107 | # revealed: , Bar> +108 | reveal_type(super()) +109 | +110 | def pls_dont_call_me(self: Never): +111 | # revealed: , Unknown> +112 | reveal_type(super()) +113 | +114 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): +115 | # revealed: , Bar> +116 | reveal_type(super()) +117 | +118 | class P(Protocol): +119 | def method(self: P): +120 | # revealed: , P> +121 | reveal_type(super()) +122 | +123 | class E(enum.Enum): +124 | X = 1 +125 | +126 | def method(self: E): +127 | match self: +128 | case E.X: +129 | # revealed: , E> +130 | reveal_type(super()) ``` # Diagnostics ``` error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` in `super(, S@method7)` call - --> src/mdtest_snippet.py:83:21 + --> src/mdtest_snippet.py:82:21 | -81 | # error: [invalid-super-argument] -82 | # revealed: Unknown -83 | reveal_type(super()) +80 | # error: [invalid-super-argument] +81 | # revealed: Unknown +82 | reveal_type(super()) | ^^^^^^^ -84 | return self +83 | return self | info: Type variable `S` has `object` as its implicit upper bound info: `object` is not an instance or subclass of `` @@ -166,13 +165,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method8` is not an instance or subclass of `` in `super(, S@method8)` call - --> src/mdtest_snippet.py:89:21 + --> src/mdtest_snippet.py:88:21 | -87 | # error: [invalid-super-argument] -88 | # revealed: Unknown -89 | reveal_type(super()) +86 | # error: [invalid-super-argument] +87 | # revealed: Unknown +88 | reveal_type(super()) | ^^^^^^^ -90 | return self +89 | return self | info: Type variable `S` has upper bound `int` info: `int` is not an instance or subclass of `` @@ -182,13 +181,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method9` is not an instance or subclass of `` in `super(, S@method9)` call - --> src/mdtest_snippet.py:95:21 + --> src/mdtest_snippet.py:94:21 | -93 | # error: [invalid-super-argument] -94 | # revealed: Unknown -95 | reveal_type(super()) +92 | # error: [invalid-super-argument] +93 | # revealed: Unknown +94 | reveal_type(super()) | ^^^^^^^ -96 | return self +95 | return self | info: Type variable `S` has constraints `int, str` info: `int | str` is not an instance or subclass of `` @@ -198,13 +197,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method10` is a type variable with an abstract/structural type as its bounds or constraints, in `super(, S@method10)` call - --> src/mdtest_snippet.py:101:21 + --> src/mdtest_snippet.py:100:21 | - 99 | # error: [invalid-super-argument] -100 | # revealed: Unknown -101 | reveal_type(super()) + 98 | # error: [invalid-super-argument] + 99 | # revealed: Unknown +100 | reveal_type(super()) | ^^^^^^^ -102 | return self +101 | return self | info: Type variable `S` has upper bound `(...) -> str` info: rule `invalid-super-argument` is enabled by default diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 23c38444c8..d0efb0db19 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7324,7 +7324,9 @@ impl<'db> Type<'db> { }); }; - Ok(typing_self(db, scope_id, typevar_binding_context, class).unwrap_or(*self)) + Ok(typing_self(db, scope_id, typevar_binding_context, class) + .map(Type::TypeVar) + .unwrap_or(*self)) } // We ensure that `typing.TypeAlias` used in the expected position (annotating an // annotated assignment statement) doesn't reach here. Using it in any other type diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 32488389bb..b4f2f90680 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -86,7 +86,7 @@ pub(crate) fn typing_self<'db>( function_scope_id: ScopeId, typevar_binding_context: Option>, class: ClassLiteral<'db>, -) -> Option> { +) -> Option> { let index = semantic_index(db, function_scope_id.file(db)); let identity = TypeVarIdentity::new( @@ -117,7 +117,6 @@ pub(crate) fn typing_self<'db>( typevar_binding_context, typevar, ) - .map(Type::TypeVar) } #[derive(Clone, Copy, Debug)] diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b30dac0ac2..06cb805c15 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2702,21 +2702,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let function_node = function_definition.node(self.module()); let function_name = &function_node.name; - // TODO: handle implicit type of `cls` for classmethods - if is_implicit_classmethod(function_name) || is_implicit_staticmethod(function_name) { + if is_implicit_staticmethod(function_name) { return None; } + let mut is_classmethod = is_implicit_classmethod(function_name); let inference = infer_definition_types(db, method_definition); for decorator in &function_node.decorator_list { let decorator_ty = inference.expression_type(&decorator.expression); - if decorator_ty.as_class_literal().is_some_and(|class| { - matches!( - class.known(db), - Some(KnownClass::Classmethod | KnownClass::Staticmethod) - ) - }) { - return None; + if let Some(known_class) = decorator_ty + .as_class_literal() + .and_then(|class| class.known(db)) + { + if known_class == KnownClass::Staticmethod { + return None; + } + + is_classmethod |= known_class == KnownClass::Classmethod; } } @@ -2726,7 +2728,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .inner_type() .as_class_literal()?; - typing_self(db, self.scope(), Some(method_definition), class_literal) + let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); + if is_classmethod { + typing_self + .map(|typing_self| SubclassOfType::from(db, SubclassOfInner::TypeVar(typing_self))) + } else { + typing_self.map(Type::TypeVar) + } } /// Set initial declared/inferred types for a `**kwargs` keyword-variadic parameter. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index f5406798a5..028087ceff 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1702,6 +1702,7 @@ impl<'db> Parameters<'db> { Some( typing_self(db, scope_id, typevar_binding_context, class) + .map(Type::TypeVar) .expect("We should always find the surrounding class for an implicit self: Self annotation"), ) } else { From 7bf50e70a78b6d725317514a97867b47308a4276 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 10 Dec 2025 14:58:57 +0100 Subject: [PATCH 05/36] [ty] Generics: Respect typevar bounds when matching against a union (#21893) ## Summary Respect typevar bounds and constraints when matching against a union. For example: ```py def accepts_t_or_int[T_str: str](x: T_str | int) -> T_str: raise NotImplementedError reveal_type(accepts_t_or_int("a")) # ok, reveals `Literal["a"]` reveal_type(accepts_t_or_int(1)) # ok, reveals `Unknown` class Unrelated: ... # error: [invalid-argument-type] "Argument type `Unrelated` does not # satisfy upper bound `str` of type variable `T_str`" accepts_t_or_int(Unrelated()) ``` Previously, the last call succeed without any errors. Worse than that, we also incorrectly solved `T_str = Unrelated`, which often lead to downstream errors. closes https://github.com/astral-sh/ty/issues/1837 ## Ecosystem impact Looks good! * Lots of removed false positives, often because we previously selected a wrong overload for a generic function (because we didn't respect the typevar bound in an earlier overload). * We now understand calls to functions accepting an argument of type `GenericPath: TypeAlias = AnyStr | PathLike[AnyStr]`. Previously, we would incorrectly match a `Path` argument against the `AnyStr` typevar (violating its constraints), but now we match against `PathLike`. ## Performance Another regression on `colour`. This package uses `numpy` heavily. And `numpy` is the codebase that originally lead me to this bug. The fix here allows us to infer more precise `np.array` types in some cases, so it's reasonable that we just need to perform more work. The fix here also requires us to look at more union elements when we would previously short-circuit incorrectly, so some more work needs to be done in the solver. ## Test Plan New Markdown tests --- .../mdtest/generics/legacy/functions.md | 38 +++++++++++++++++++ .../mdtest/generics/pep695/functions.md | 32 ++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 30 ++++++--------- 3 files changed, 82 insertions(+), 18 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 6e89253bd0..69e079ca65 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -337,6 +337,44 @@ reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] ``` +This also works if the typevar has a bound: + +```py +T_str = TypeVar("T_str", bound=str) + +def accepts_t_or_int(x: T_str | int) -> T_str: + raise NotImplementedError + +reveal_type(accepts_t_or_int("a")) # revealed: Literal["a"] +reveal_type(accepts_t_or_int(1)) # revealed: Unknown + +class Unrelated: ... + +# error: [invalid-argument-type] "Argument type `Unrelated` does not satisfy upper bound `str` of type variable `T_str`" +reveal_type(accepts_t_or_int(Unrelated())) # revealed: Unknown +``` + +```py +T_str = TypeVar("T_str", bound=str) + +def accepts_t_or_list_of_t(x: T_str | list[T_str]) -> T_str: + raise NotImplementedError + +reveal_type(accepts_t_or_list_of_t("a")) # revealed: Literal["a"] +# error: [invalid-argument-type] "Argument type `Literal[1]` does not satisfy upper bound `str` of type variable `T_str`" +reveal_type(accepts_t_or_list_of_t(1)) # revealed: Unknown + +def _(list_ofstr: list[str], list_of_int: list[int]): + reveal_type(accepts_t_or_list_of_t(list_ofstr)) # revealed: str + + # TODO: the error message here could be improved by referring to the second union element + # error: [invalid-argument-type] "Argument type `list[int]` does not satisfy upper bound `str` of type variable `T_str`" + reveal_type(accepts_t_or_list_of_t(list_of_int)) # revealed: Unknown +``` + +Here, we make sure that `S` is solved as `Literal[1]` instead of a union of the two literals, which +would also be a valid solution: + ```py S = TypeVar("S") 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 843bd60d21..eedea0beaa 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -302,6 +302,38 @@ reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] ``` +This also works if the typevar has a bound: + +```py +def accepts_t_or_int[T_str: str](x: T_str | int) -> T_str: + raise NotImplementedError + +reveal_type(accepts_t_or_int("a")) # revealed: Literal["a"] +reveal_type(accepts_t_or_int(1)) # revealed: Unknown + +class Unrelated: ... + +# error: [invalid-argument-type] "Argument type `Unrelated` does not satisfy upper bound `str` of type variable `T_str`" +reveal_type(accepts_t_or_int(Unrelated())) # revealed: Unknown + +def accepts_t_or_list_of_t[T: str](x: T | list[T]) -> T: + raise NotImplementedError + +reveal_type(accepts_t_or_list_of_t("a")) # revealed: Literal["a"] +# error: [invalid-argument-type] "Argument type `Literal[1]` does not satisfy upper bound `str` of type variable `T`" +reveal_type(accepts_t_or_list_of_t(1)) # revealed: Unknown + +def _(list_ofstr: list[str], list_of_int: list[int]): + reveal_type(accepts_t_or_list_of_t(list_ofstr)) # revealed: str + + # TODO: the error message here could be improved by referring to the second union element + # error: [invalid-argument-type] "Argument type `list[int]` does not satisfy upper bound `str` of type variable `T`" + reveal_type(accepts_t_or_list_of_t(list_of_int)) # revealed: Unknown +``` + +Here, we make sure that `S` is solved as `Literal[1]` instead of a union of the two literals, which +would also be a valid solution: + ```py def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]: return y diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index b4f2f90680..ade46d77ca 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1570,21 +1570,9 @@ impl<'db> SpecializationBuilder<'db> { let mut bound_typevars = (union_formal.elements(self.db).iter()).filter_map(|ty| ty.as_typevar()); - let first_bound_typevar = bound_typevars.next(); - let has_more_than_one_typevar = bound_typevars.next().is_some(); - - // Otherwise, if precisely one union element _is_ a typevar (not _contains_ a - // typevar), then we add a mapping between that typevar and the actual type. - if let Some(bound_typevar) = first_bound_typevar - && !has_more_than_one_typevar - { - self.add_type_mapping(bound_typevar, actual, polarity, f); - return Ok(()); - } - // TODO: // Handling more than one bare typevar is something that we can't handle yet. - if has_more_than_one_typevar { + if bound_typevars.nth(1).is_some() { return Ok(()); } @@ -1599,15 +1587,21 @@ impl<'db> SpecializationBuilder<'db> { let mut first_error = None; let mut found_matching_element = false; for formal_element in union_formal.elements(self.db) { - if !formal_element.is_disjoint_from(self.db, actual) { - let result = self.infer_map_impl(*formal_element, actual, polarity, &mut f); - if let Err(err) = result { - first_error.get_or_insert(err); - } else { + let result = self.infer_map_impl(*formal_element, actual, polarity, &mut f); + if let Err(err) = result { + first_error.get_or_insert(err); + } else { + // The recursive call to `infer_map_impl` may succeed even if the actual type is + // not assignable to the formal element. + if !actual + .when_assignable_to(self.db, *formal_element, self.inferable) + .is_never_satisfied(self.db) + { found_matching_element = true; } } } + if !found_matching_element && let Some(error) = first_error { return Err(error); } From 951766d1fbce5bfa6758f81876ae26124892f33e Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 10 Dec 2025 08:18:08 -0800 Subject: [PATCH 06/36] [ty] default-specialize class-literal types in assignment to generic-alias types (#21883) Fixes https://github.com/astral-sh/ty/issues/1832, fixes https://github.com/astral-sh/ty/issues/1513 ## Summary A class object `C` (for which we infer an unspecialized `ClassLiteral` type) should always be assignable to the type `type[C]` (which is default-specialized, if `C` is generic). We already implemented this for most cases, but we missed the case of a generic final type, where we simplify `type[C]` to the `GenericAlias` type for the default specialization of `C`. So we also need to implement this assignability of generic `ClassLiteral` types as-if default-specialized. ## Test Plan Added mdtests that failed before this PR. --------- Co-authored-by: David Peter --- .../resources/mdtest/narrow/issubclass.md | 42 +++++ .../resources/mdtest/type_of/basic.md | 55 ------ .../resources/mdtest/type_of/generics.md | 177 ++++++++++++++++++ .../type_properties/is_assignable_to.md | 34 ++++ .../type_properties/is_disjoint_from.md | 42 +++++ crates/ty_python_semantic/src/types.rs | 28 +++ 6 files changed, 323 insertions(+), 55 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index ed9964274a..6c45c19ef2 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -220,6 +220,48 @@ def f(x: type[int | str | bytes | range]): reveal_type(x) # revealed: ``` +## `classinfo` is a generic final class + +```toml +[environment] +python-version = "3.12" +``` + +When we check a generic `@final` class against `type[GenericFinal]`, we can conclude that the check +always succeeds: + +```py +from typing import final + +@final +class GenericFinal[T]: + x: T # invariant + +def f(x: type[GenericFinal]): + reveal_type(x) # revealed: + + if issubclass(x, GenericFinal): + reveal_type(x) # revealed: + else: + reveal_type(x) # revealed: Never +``` + +This also works if the typevar has an upper bound: + +```py +@final +class BoundedGenericFinal[T: int]: + x: T # invariant + +def g(x: type[BoundedGenericFinal]): + reveal_type(x) # revealed: + + if issubclass(x, BoundedGenericFinal): + reveal_type(x) # revealed: + else: + reveal_type(x) # revealed: Never +``` + ## Special cases ### Emit a diagnostic if the first argument is of wrong type 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 4e3d9e9f07..da89cc4c16 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -152,61 +152,6 @@ class Foo(type[int]): ... reveal_mro(Foo) # revealed: (, , ) ``` -## Display of generic `type[]` types - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import Generic, TypeVar - -class Foo[T]: ... - -S = TypeVar("S") - -class Bar(Generic[S]): ... - -def _(x: Foo[int], y: Bar[str], z: list[bytes]): - reveal_type(type(x)) # revealed: type[Foo[int]] - reveal_type(type(y)) # revealed: type[Bar[str]] - reveal_type(type(z)) # revealed: type[list[bytes]] -``` - -## Checking generic `type[]` types - -```toml -[environment] -python-version = "3.12" -``` - -```py -class C[T]: - pass - -class D[T]: - pass - -var: type[C[int]] = C[int] -var: type[C[int]] = D[int] # error: [invalid-assignment] "Object of type `` is not assignable to `type[C[int]]`" -``` - -However, generic `Protocol` classes are still TODO: - -```py -from typing import Protocol - -class Proto[U](Protocol): - def some_method(self): ... - -# TODO: should be error: [invalid-assignment] -var: type[Proto[int]] = C[int] - -def _(p: type[Proto[int]]): - reveal_type(p) # revealed: type[@Todo(type[T] for protocols)] -``` - ## `@final` classes `type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is 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 3c8f157dad..198390adb9 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -274,3 +274,180 @@ class Foo[T]: ... # error: [invalid-parameter-default] "Default value of type `` is not assignable to annotated parameter type `type[T@f]`" def f[T: Foo[Any]](x: type[T] = Foo): ... ``` + +## Display of generic `type[]` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar + +class Foo[T]: ... + +S = TypeVar("S") + +class Bar(Generic[S]): ... + +def _(x: Foo[int], y: Bar[str], z: list[bytes]): + reveal_type(type(x)) # revealed: type[Foo[int]] + reveal_type(type(y)) # revealed: type[Bar[str]] + reveal_type(type(z)) # revealed: type[list[bytes]] +``` + +## Checking generic `type[]` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C[T]: + pass + +class D[T]: + pass + +var: type[C[int]] = C[int] +var: type[C[int]] = D[int] # error: [invalid-assignment] "Object of type `` is not assignable to `type[C[int]]`" +``` + +However, generic `Protocol` classes are still TODO: + +```py +from typing import Protocol + +class Proto[U](Protocol): + def some_method(self): ... + +# TODO: should be error: [invalid-assignment] +var: type[Proto[int]] = C[int] + +def _(p: type[Proto[int]]): + reveal_type(p) # revealed: type[@Todo(type[T] for protocols)] +``` + +## Generic `@final` classes + +```toml +[environment] +python-version = "3.13" +``` + +An unspecialized generic final class object is assignable to its default-specialized `type[]` type +(which is actually internally simplified to a GenericAlias type, since there cannot be subclasses.) + +```py +from typing import final + +@final +class P[T]: + x: T + +def expects_type_p(x: type[P]): + pass + +def expects_type_p_of_int(x: type[P[int]]): + pass + +# OK, the default specialization of `P` is assignable to `type[P[Unknown]]` +expects_type_p(P) + +# Also OK, because `P[int]` and `P[str]` are both assignable to `P[Unknown]` +expects_type_p(P[int]) +expects_type_p(P[str]) + +# Also OK, because the default specialization is `P[Unknown]` which is assignable to `P[int]` +expects_type_p_of_int(P) +expects_type_p_of_int(P[int]) + +# Not OK, because `P[str]` is not assignable to `P[int]` +expects_type_p_of_int(P[str]) # error: [invalid-argument-type] +``` + +The same principles apply when typevar defaults are used, but the results are a bit different +because the default-specialization is no longer a forgiving `Unknown` type: + +```py +@final +class P[T = str]: + x: T + +def expects_type_p(x: type[P]): + pass + +def expects_type_p_of_int(x: type[P[int]]): + pass + +def expects_type_p_of_str(x: type[P[str]]): + pass + +# OK, the default specialization is now `P[str]`, but we have the default specialization on both +# sides, so it is assignable. +expects_type_p(P) + +# Also OK if the explicit specialization lines up with the default, in either direction: +expects_type_p(P[str]) +expects_type_p_of_str(P) +expects_type_p_of_str(P[str]) + +# Not OK if the specializations don't line up: +expects_type_p(P[int]) # error: [invalid-argument-type] +expects_type_p_of_int(P[str]) # error: [invalid-argument-type] +expects_type_p_of_int(P) # error: [invalid-argument-type] +expects_type_p_of_str(P[int]) # error: [invalid-argument-type] +``` + +This also works with `ParamSpec`: + +```py +@final +class C[**P]: ... + +def expects_type_c(f: type[C]): ... +def expects_type_c_of_int_and_str(x: type[C[int, str]]): ... + +# OK, the unspecialized `C` is assignable to `type[C[...]]` +expects_type_c(C) + +# Also OK, any specialization is assignable to the unspecialized `C` +expects_type_c(C[int]) +expects_type_c(C[str, int, bytes]) + +# Ok, the unspecialized `C` is assignable to `type[C[int, str]]` +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 +expects_type_c_of_int_and_str(C[str]) +expects_type_c_of_int_and_str(C[int, str, bytes]) +expects_type_c_of_int_and_str(C[str, int]) +``` + +And with a `ParamSpec` that has a default: + +```py +@final +class C[**P = [int, str]]: ... + +def expects_type_c_default(f: type[C]): ... +def expects_type_c_default_of_int(f: type[C[int]]): ... +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 +expects_type_c_default(C[int]) +expects_type_c_default_of_int(C[str]) +expects_type_c_default_of_int_str(C[str, 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 f9f85641f7..f2e38485c5 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 @@ -1338,6 +1338,40 @@ def g3(obj: Foo[tuple[A]]): f3(obj) ``` +## Generic aliases + +```py +from typing import final +from ty_extensions import static_assert, is_assignable_to, TypeOf + +class GenericClass[T]: + x: T # invariant + +static_assert(is_assignable_to(TypeOf[GenericClass], type[GenericClass])) +static_assert(is_assignable_to(TypeOf[GenericClass[int]], type[GenericClass])) +static_assert(is_assignable_to(TypeOf[GenericClass], type[GenericClass[int]])) +static_assert(is_assignable_to(TypeOf[GenericClass[int]], type[GenericClass[int]])) +static_assert(not is_assignable_to(TypeOf[GenericClass[str]], type[GenericClass[int]])) + +class GenericClassIntBound[T: int]: + x: T # invariant + +static_assert(is_assignable_to(TypeOf[GenericClassIntBound], type[GenericClassIntBound])) +static_assert(is_assignable_to(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound])) +static_assert(is_assignable_to(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) +static_assert(is_assignable_to(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) + +@final +class GenericFinalClass[T]: + x: T # invariant + +static_assert(is_assignable_to(TypeOf[GenericFinalClass], type[GenericFinalClass])) +static_assert(is_assignable_to(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) +static_assert(is_assignable_to(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) +static_assert(is_assignable_to(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[int]])) +static_assert(not is_assignable_to(TypeOf[GenericFinalClass[str]], type[GenericFinalClass[int]])) +``` + ## `TypeGuard` and `TypeIs` `TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`. 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 db4b0f5f98..166d67edd0 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 @@ -666,6 +666,48 @@ 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, is_disjoint_from, TypeOf + +class GenericClass[T]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass])) +# TODO: these should not error +static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) # error: [static-assert-error] +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])) +# TODO: these should not error +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) + +@final +class GenericFinalClass[T]: + x: T # invariant + +# TODO: these should not error +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) # error: [static-assert-error] +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 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d0efb0db19..12a38a8e34 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2709,6 +2709,34 @@ impl<'db> Type<'db> { ) }) .unwrap_or_else(|| ConstraintSet::from(relation.is_assignability())), + + // Similarly, `` is assignable to `` (a generic-alias type) + // if the default specialization of `C` is assignable to `C[...]`. This scenario occurs + // with final generic types, where `type[C[...]]` is simplified to the generic-alias + // type ``, due to the fact that `C[...]` has no subclasses. + (Type::ClassLiteral(class), Type::GenericAlias(target_alias)) => { + class.default_specialization(db).has_relation_to_impl( + db, + ClassType::Generic(target_alias), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + } + + // For generic aliases, we delegate to the underlying class type. + (Type::GenericAlias(self_alias), Type::GenericAlias(target_alias)) => { + ClassType::Generic(self_alias).has_relation_to_impl( + db, + ClassType::Generic(target_alias), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + } + (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class(db) From 2dd412c89ae59b19708d4f43e879f5c16c50e6b2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 10 Dec 2025 17:25:41 +0100 Subject: [PATCH 07/36] Update README to remove production warning (#21899) --- crates/ty/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ty/README.md b/crates/ty/README.md index fb98292a0f..5c766624c3 100644 --- a/crates/ty/README.md +++ b/crates/ty/README.md @@ -1,7 +1,6 @@ # ty ty is an extremely fast type checker. -Currently, it is a work-in-progress and not ready for production use. The Rust code for ty lives in this repository; see [CONTRIBUTING.md](CONTRIBUTING.md) for more information on contributing to ty. From 9ceec359a04798415716ac0d7ac2260524dc46ce Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 10 Dec 2025 17:37:17 +0100 Subject: [PATCH 08/36] [ty] Add mypy primer check comparing same revisions (#21864) --- .github/workflows/mypy_primer.yaml | 53 ++++++++++++++++++++++++++++++ scripts/mypy_primer.sh | 6 ++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index b2f7f5e275..22af7025da 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -47,6 +47,7 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: + shared-key: "mypy-primer" workspaces: "ruff" - name: Install Rust toolchain @@ -86,6 +87,7 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: "ruff" + shared-key: "mypy-primer" - name: Install Rust toolchain run: rustup show @@ -105,3 +107,54 @@ jobs: with: name: mypy_primer_memory_diff path: mypy_primer_memory.diff + + # Runs mypy twice against the same ty version to catch any non-deterministic behavior (ideally). + # The job is disabled for now because there are some non-deterministic diagnostics. + mypy_primer_same_revision: + name: Run mypy_primer on same revision + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} + timeout-minutes: 20 + # TODO: Enable once we fixed the non-deterministic diagnostics + if: false + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + path: ruff + fetch-depth: 0 + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + workspaces: "ruff" + shared-key: "mypy-primer" + + - name: Install Rust toolchain + run: rustup show + + - name: Run determinism check + env: + BASE_REVISION: ${{ github.event.pull_request.head.sha }} + PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt + CLICOLOR_FORCE: "1" + DIFF_FILE: mypy_primer_determinism.diff + run: | + cd ruff + scripts/mypy_primer.sh + + - name: Check for non-determinism + run: | + # Remove ANSI color codes for checking + sed -e 's/\x1b\[[0-9;]*m//g' mypy_primer_determinism.diff > mypy_primer_determinism_clean.diff + + # Check if there are any differences (non-determinism) + if [ -s mypy_primer_determinism_clean.diff ]; then + echo "ERROR: Non-deterministic output detected!" + echo "The following differences were found when running ty twice on the same commit:" + cat mypy_primer_determinism_clean.diff + exit 1 + else + echo "✓ Output is deterministic" + fi diff --git a/scripts/mypy_primer.sh b/scripts/mypy_primer.sh index 527376c12b..7af2501f03 100755 --- a/scripts/mypy_primer.sh +++ b/scripts/mypy_primer.sh @@ -10,8 +10,10 @@ PRIMER_SELECTOR="$(paste -s -d'|' "${PRIMER_SELECTOR}")" echo "new commit" git rev-list --format=%s --max-count=1 "${GITHUB_SHA}" -MERGE_BASE="$(git merge-base "${GITHUB_SHA}" "origin/${GITHUB_BASE_REF}")" -git checkout -b base_commit "${MERGE_BASE}" +if [ -z "${BASE_REVISION:-}" ]; then + BASE_REVISION="$(git merge-base "${GITHUB_SHA}" "origin/${GITHUB_BASE_REF}")" +fi +git checkout -b base_commit "${BASE_REVISION}" echo "base commit" git rev-list --format=%s --max-count=1 base_commit From 59b92b3522338aba99f131d4e8f23beb7fd66c3d Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 10 Dec 2025 11:43:55 -0500 Subject: [PATCH 09/36] Document `*.pyw` is included by default in preview (#21885) Document `*.pyw` is included by default in preview mode. Originally requested in https://github.com/astral-sh/ruff/issues/13246 and added in https://github.com/astral-sh/ruff/pull/20458 Co-authored-by: Amethyst Reese --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 3420611e18..a88f71488f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -352,6 +352,7 @@ For example, without `force-exclude` enabled, `ruff check /path/to/excluded/file ### Default inclusions By default, Ruff will discover files matching `*.py`, `*.pyi`, `*.ipynb`, or `pyproject.toml`. +In [preview](preview.md) mode, Ruff will also discover `*.pyw` by default. To lint or format files with additional file extensions, use the [`extend-include`](settings.md#extend-include) setting. You can also change the default selection using the [`include`](settings.md#include) setting. From f7528bd325e20205facad414cf8462e922bbffa8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 10 Dec 2025 17:47:41 +0100 Subject: [PATCH 10/36] [ty] Checking files without extension (#21867) --- crates/ty/tests/cli/main.rs | 47 +++++++++++++ crates/ty_project/src/walk.rs | 15 ++-- crates/ty_server/src/session.rs | 17 ++++- crates/ty_server/src/session/index.rs | 15 ++-- crates/ty_server/tests/e2e/main.rs | 1 - .../tests/e2e/publish_diagnostics.rs | 68 +++++++++++++++++- ..._language_of_file_without_extension-2.snap | 21 ++++++ ...ng_language_of_file_without_extension.snap | 70 +++++++++++++++++++ ...without_extension_but_python_language.snap | 70 +++++++++++++++++++ 9 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-2.snap create mode 100644 crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension.snap create mode 100644 crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open_file_without_extension_but_python_language.snap diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index a352ba355a..546d3ac260 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -580,6 +580,53 @@ fn check_non_existing_path() -> anyhow::Result<()> { Ok(()) } +#[test] +fn check_file_without_extension() -> anyhow::Result<()> { + let case = CliTest::with_file("main", "a = b")?; + + assert_cmd_snapshot!( + case.command().arg("main"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `b` used when not defined + --> main:1:5 + | + 1 | a = b + | ^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + " + ); + + Ok(()) +} + +#[test] +fn check_file_without_extension_in_subfolder() -> anyhow::Result<()> { + let case = CliTest::with_file("src/main", "a = b")?; + + assert_cmd_snapshot!( + case.command().arg("src"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN No python files found under the given path(s) + " + ); + + Ok(()) +} + #[test] fn concise_diagnostics() -> anyhow::Result<()> { let case = CliTest::with_file( diff --git a/crates/ty_project/src/walk.rs b/crates/ty_project/src/walk.rs index 8c9958c416..16a73dd65f 100644 --- a/crates/ty_project/src/walk.rs +++ b/crates/ty_project/src/walk.rs @@ -202,11 +202,16 @@ impl<'a> ProjectFilesWalker<'a> { } } else { // Ignore any non python files to avoid creating too many entries in `Files`. - if entry - .path() - .extension() - .and_then(PySourceType::try_from_extension) - .is_none() + // Unless the file is explicitly passed, we then always assume it's a python file. + let source_type = entry.path().extension().and_then(PySourceType::try_from_extension).or_else(|| { + if entry.depth() == 0 { + Some(PySourceType::Python) + } else { + db.system().source_type(entry.path()) + } + }); + + if source_type.is_none() { return WalkState::Continue; } diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 233b0e6aa3..4ebfe7ead0 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -20,6 +20,7 @@ use lsp_types::{ use ruff_db::Db; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_python_ast::PySourceType; use ty_combine::Combine; use ty_project::metadata::Options; use ty_project::watch::{ChangeEvent, CreatedKind}; @@ -1522,7 +1523,8 @@ impl DocumentHandle { pub(crate) fn close(&self, session: &mut Session) -> crate::Result { let is_cell = self.is_cell(); let path = self.notebook_or_file_path(); - session.index_mut().close_document(&self.key())?; + + let removed_document = session.index_mut().close_document(&self.key())?; // Close the text or notebook file in the database but skip this // step for cells because closing a cell doesn't close its notebook. @@ -1535,6 +1537,19 @@ impl DocumentHandle { AnySystemPath::System(system_path) => { if let Some(file) = db.files().try_system(db, system_path) { db.project().close_file(db, file); + + // In case we preferred the language given by the Client + // over the one detected by the file extension, remove the file + // from the project to handle cases where a user changes the language + // of a file (which results in a didClose and didOpen for the same path but with different languages). + if removed_document.language_id().is_some() + && system_path + .extension() + .and_then(PySourceType::try_from_extension) + .is_none() + { + db.project().remove_file(db, file); + } } else { // This can only fail when the path is a directory or it doesn't exists but the // file should exists for this handler in this branch. This is because every diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index 6a34fb4ea2..95237212cf 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -1,7 +1,7 @@ use rustc_hash::FxHashMap; use std::sync::Arc; -use crate::document::DocumentKey; +use crate::document::{DocumentKey, LanguageId}; use crate::session::DocumentHandle; use crate::{ PositionEncoding, TextDocument, @@ -187,12 +187,12 @@ impl Index { handle } - pub(super) fn close_document(&mut self, key: &DocumentKey) -> Result<(), DocumentError> { - let Some(_) = self.documents.remove(key) else { + pub(super) fn close_document(&mut self, key: &DocumentKey) -> Result { + let Some(document) = self.documents.remove(key) else { return Err(DocumentError::NotFound(key.clone())); }; - Ok(()) + Ok(document) } pub(super) fn document_mut( @@ -229,6 +229,13 @@ impl Document { } } + pub(crate) fn language_id(&self) -> Option { + match self { + Self::Text(document) => document.language_id(), + Self::Notebook(_) => None, + } + } + pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { Some(match self { Self::Notebook(notebook) => Arc::make_mut(notebook), diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 5ef434cef6..579e730129 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -787,7 +787,6 @@ impl TestServer { } /// Send a `textDocument/didClose` notification - #[expect(dead_code)] pub(crate) fn close_text_document(&mut self, path: impl AsRef) { let params = DidCloseTextDocumentParams { text_document: TextDocumentIdentifier { diff --git a/crates/ty_server/tests/e2e/publish_diagnostics.rs b/crates/ty_server/tests/e2e/publish_diagnostics.rs index 64580bc88c..bbe7094325 100644 --- a/crates/ty_server/tests/e2e/publish_diagnostics.rs +++ b/crates/ty_server/tests/e2e/publish_diagnostics.rs @@ -1,7 +1,10 @@ use std::time::Duration; use anyhow::Result; -use lsp_types::{FileChangeType, FileEvent, notification::PublishDiagnostics}; +use lsp_types::{ + DidOpenTextDocumentParams, FileChangeType, FileEvent, TextDocumentItem, + notification::{DidOpenTextDocument, PublishDiagnostics}, +}; use ruff_db::system::SystemPath; use crate::TestServerBuilder; @@ -160,3 +163,66 @@ def foo() -> str: Ok(()) } + +#[test] +fn on_did_open_file_without_extension_but_python_language() -> Result<()> { + let foo = SystemPath::new("src/foo"); + let foo_content = "\ +def foo() -> str: + return 42 +"; + + let mut server = TestServerBuilder::new()? + .with_workspace(SystemPath::new("src"), None)? + .with_file(foo, foo_content)? + .enable_pull_diagnostics(false) + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document(foo, foo_content, 1); + let diagnostics = server.await_notification::(); + + insta::assert_debug_snapshot!(diagnostics); + + Ok(()) +} + +#[test] +fn changing_language_of_file_without_extension() -> Result<()> { + let foo = SystemPath::new("src/foo"); + let foo_content = "\ +def foo() -> str: + return 42 +"; + + let mut server = TestServerBuilder::new()? + .with_workspace(SystemPath::new("src"), None)? + .with_file(foo, foo_content)? + .enable_pull_diagnostics(false) + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document(foo, foo_content, 1); + let diagnostics = server.await_notification::(); + + insta::assert_debug_snapshot!(diagnostics); + + server.close_text_document(foo); + + let params = DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: server.file_uri(foo), + language_id: "text".to_string(), + version: 1, + text: foo_content.to_string(), + }, + }; + server.send_notification::(params); + let _close_diagnostics = server.await_notification::(); + + let diagnostics = server.await_notification::(); + + insta::assert_debug_snapshot!(diagnostics); + + Ok(()) +} diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-2.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-2.snap new file mode 100644 index 0000000000..91a4a10b81 --- /dev/null +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-2.snap @@ -0,0 +1,21 @@ +--- +source: crates/ty_server/tests/e2e/publish_diagnostics.rs +expression: diagnostics +--- +PublishDiagnosticsParams { + uri: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/src/foo", + query: None, + fragment: None, + }, + diagnostics: [], + version: Some( + 1, + ), +} diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension.snap new file mode 100644 index 0000000000..d6a9af02de --- /dev/null +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension.snap @@ -0,0 +1,70 @@ +--- +source: crates/ty_server/tests/e2e/publish_diagnostics.rs +expression: diagnostics +--- +PublishDiagnosticsParams { + uri: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/src/foo", + query: None, + fragment: None, + }, + diagnostics: [ + Diagnostic { + range: Range { + start: Position { + line: 1, + character: 11, + }, + end: Position { + line: 1, + character: 13, + }, + }, + severity: Some( + Error, + ), + code: Some( + String( + "invalid-return-type", + ), + ), + code_description: Some( + CodeDescription { + href: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "ty.dev", + ), + ), + port: None, + path: "/rules", + query: None, + fragment: Some( + "invalid-return-type", + ), + }, + }, + ), + source: Some( + "ty", + ), + message: "Return type does not match returned value: expected `str`, found `Literal[42]`", + related_information: None, + tags: None, + data: None, + }, + ], + version: Some( + 1, + ), +} diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open_file_without_extension_but_python_language.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open_file_without_extension_but_python_language.snap new file mode 100644 index 0000000000..d6a9af02de --- /dev/null +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open_file_without_extension_but_python_language.snap @@ -0,0 +1,70 @@ +--- +source: crates/ty_server/tests/e2e/publish_diagnostics.rs +expression: diagnostics +--- +PublishDiagnosticsParams { + uri: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/src/foo", + query: None, + fragment: None, + }, + diagnostics: [ + Diagnostic { + range: Range { + start: Position { + line: 1, + character: 11, + }, + end: Position { + line: 1, + character: 13, + }, + }, + severity: Some( + Error, + ), + code: Some( + String( + "invalid-return-type", + ), + ), + code_description: Some( + CodeDescription { + href: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "ty.dev", + ), + ), + port: None, + path: "/rules", + query: None, + fragment: Some( + "invalid-return-type", + ), + }, + }, + ), + source: Some( + "ty", + ), + message: "Return type does not match returned value: expected `str`, found `Literal[42]`", + related_information: None, + tags: None, + data: None, + }, + ], + version: Some( + 1, + ), +} From 5dc0079e789d3a895ac908fd5f277a37ca35f373 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 10 Dec 2025 14:17:22 -0500 Subject: [PATCH 11/36] [ty] Fix disjointness checks on `@final` class instances (#21769) ## Summary This was left unfinished in https://github.com/astral-sh/ruff/pull/21167. This is required to fix our disjointness checks with type-of a final class, which is currently broken, and blocking https://github.com/astral-sh/ty/issues/159. --- .../type_properties/is_disjoint_from.md | 7 +- crates/ty_python_semantic/src/types/class.rs | 36 +++++-- .../ty_python_semantic/src/types/generics.rs | 93 ++++++++++++++++--- crates/ty_python_semantic/src/types/tuple.rs | 17 ++++ 4 files changed, 132 insertions(+), 21 deletions(-) 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 166d67edd0..02f50cc412 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 @@ -95,7 +95,7 @@ python-version = "3.12" ``` ```py -from typing import final +from typing import Any, final from ty_extensions import static_assert, is_disjoint_from @final @@ -106,9 +106,12 @@ class Foo[T]: 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])) -# TODO: `int` and `str` are disjoint bases, so these should be disjoint. +# `Foo[Never]` is a subtype of both `Foo[int]` and `Foo[str]`. static_assert(not is_disjoint_from(Foo[int], Foo[str])) ``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a5122431dd..492ed63f21 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -718,6 +718,31 @@ impl<'db> ClassType<'db> { .find_map(|base| base.as_disjoint_base(db)) } + /// Return `true` if this class could exist in the MRO of `other`. + pub(super) fn could_exist_in_mro_of(self, db: &'db dyn Db, other: Self) -> bool { + other + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|class| match (self, class) { + (ClassType::NonGeneric(this_class), ClassType::NonGeneric(other_class)) => { + this_class == other_class + } + (ClassType::Generic(this_alias), ClassType::Generic(other_alias)) => { + this_alias.origin(db) == other_alias.origin(db) + && !this_alias + .specialization(db) + .is_disjoint_from( + db, + other_alias.specialization(db), + InferableTypeVars::None, + ) + .is_always_satisfied(db) + } + (ClassType::NonGeneric(_), ClassType::Generic(_)) + | (ClassType::Generic(_), ClassType::NonGeneric(_)) => false, + }) + } + /// Return `true` if this class could coexist in an MRO with `other`. /// /// For two given classes `A` and `B`, it is often possible to say for sure @@ -729,16 +754,11 @@ impl<'db> ClassType<'db> { } if self.is_final(db) { - return self - .iter_mro(db) - .filter_map(ClassBase::into_class) - .any(|class| class.class_literal(db).0 == other.class_literal(db).0); + return other.could_exist_in_mro_of(db, self); } + if other.is_final(db) { - return other - .iter_mro(db) - .filter_map(ClassBase::into_class) - .any(|class| class.class_literal(db).0 == self.class_literal(db).0); + return self.could_exist_in_mro_of(db, other); } // Two disjoint bases can only coexist in an MRO if one is a subclass of the other. diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index ade46d77ca..58ccd28ca0 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -11,7 +11,7 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind, ScopeId}; use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; -use crate::types::constraints::ConstraintSet; +use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::instance::{Protocol, ProtocolInstanceType}; use crate::types::signatures::{Parameters, ParametersKind}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; @@ -1226,18 +1226,20 @@ impl<'db> Specialization<'db> { let self_materialization_kind = self.materialization_kind(db); let other_materialization_kind = other.materialization_kind(db); - let mut result = ConstraintSet::from(true); - for ((bound_typevar, self_type), other_type) in (generic_context.variables(db)) - .zip(self.types(db)) - .zip(other.types(db)) - { + let types = itertools::izip!( + generic_context.variables(db), + self.types(db), + other.types(db) + ); + + types.when_all(db, |(bound_typevar, self_type, other_type)| { // Subtyping/assignability of each type in the specialization depends on the variance // of the corresponding typevar: // - covariant: verify that self_type <: other_type // - contravariant: verify that other_type <: self_type // - invariant: verify that self_type <: other_type AND other_type <: self_type // - bivariant: skip, can't make subtyping/assignability false - let compatible = match bound_typevar.variance(db) { + match bound_typevar.variance(db) { TypeVarVariance::Invariant => has_relation_in_invariant_position( db, self_type, @@ -1266,13 +1268,82 @@ impl<'db> Specialization<'db> { disjointness_visitor, ), TypeVarVariance::Bivariant => ConstraintSet::from(true), - }; - if result.intersect(db, compatible).is_never_satisfied(db) { - return result; } + }) + } + + pub(crate) fn is_disjoint_from( + self, + db: &'db dyn Db, + other: Self, + inferable: InferableTypeVars<'_, 'db>, + ) -> ConstraintSet<'db> { + self.is_disjoint_from_impl( + db, + other, + inferable, + &IsDisjointVisitor::default(), + &HasRelationToVisitor::default(), + ) + } + + pub(crate) fn is_disjoint_from_impl( + self, + db: &'db dyn Db, + other: Self, + inferable: InferableTypeVars<'_, 'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, + relation_visitor: &HasRelationToVisitor<'db>, + ) -> ConstraintSet<'db> { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return ConstraintSet::from(true); } - result + if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) + { + return self_tuple.is_disjoint_from_impl( + db, + other_tuple, + inferable, + disjointness_visitor, + relation_visitor, + ); + } + + let types = itertools::izip!( + generic_context.variables(db), + self.types(db), + other.types(db) + ); + + types.when_all( + db, + |(bound_typevar, self_type, other_type)| match bound_typevar.variance(db) { + // TODO: This check can lead to false negatives. + // + // For example, `Foo[int]` and `Foo[bool]` are disjoint, even though `bool` is a subtype + // of `int`. However, given two non-inferable type variables `T` and `U`, `Foo[T]` and + // `Foo[U]` should not be considered disjoint, as `T` and `U` could be specialized to the + // same type. We don't currently have a good typing relationship to represent this. + TypeVarVariance::Invariant => self_type.is_disjoint_from_impl( + db, + *other_type, + inferable, + disjointness_visitor, + relation_visitor, + ), + + // If `Foo[T]` is covariant in `T`, `Foo[Never]` is a subtype of `Foo[A]` and `Foo[B]` + TypeVarVariance::Covariant => ConstraintSet::from(false), + + // If `Foo[T]` is contravariant in `T`, `Foo[A | B]` is a subtype of `Foo[A]` and `Foo[B]` + TypeVarVariance::Contravariant => ConstraintSet::from(false), + + // If `Foo[T]` is bivariant in `T`, `Foo[A]` and `Foo[B]` are mutual subtypes. + TypeVarVariance::Bivariant => ConstraintSet::from(false), + }, + ) } pub(crate) fn is_equivalent_to_impl( diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 9b046cd4cf..f0cc3bedc8 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -288,6 +288,23 @@ impl<'db> TupleType<'db> { ) } + pub(crate) fn is_disjoint_from_impl( + self, + db: &'db dyn Db, + other: Self, + inferable: InferableTypeVars<'_, 'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, + relation_visitor: &HasRelationToVisitor<'db>, + ) -> ConstraintSet<'db> { + self.tuple(db).is_disjoint_from_impl( + db, + other.tuple(db), + inferable, + disjointness_visitor, + relation_visitor, + ) + } + pub(crate) fn is_equivalent_to_impl( self, db: &'db dyn Db, From 3e00221a6c9e5dfc1cee12e47f0aa0644fc56f00 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 10 Dec 2025 15:07:50 -0500 Subject: [PATCH 12/36] [ty] Fix negation upper bounds in constraint sets (#21897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the logic error that @sharkdp [found](https://github.com/astral-sh/ruff/pull/21871#discussion_r2605755588) in the constraint set upper bound normalization logic I introduced in #21871. I had originally claimed that `(T ≤ α & ~β)` should simplify into `(T ≤ α) ∧ ¬(T ≤ β)`. But that also suggests that `T ≤ ~β` should simplify to `¬(T ≤ β)` on its own, and that's not correct. The correct simplification is that `~α` is an "atomic" type, not an "intersection" for the purposes of our upper bound simplifcation. So `(T ≤ α & ~β)` should simplify to `(T ≤ α) ∧ (T ≤ ~β)`. That is, break apart the elements of a (proper) intersection, regardless of whether each element is negated or not. This PR fixes the logic, adds a test case, and updates the comments to be hopefully more clear and accurate. --- .../mdtest/type_properties/constraints.md | 11 +++++++- crates/ty_python_semantic/src/types.rs | 14 ++++++++-- .../src/types/constraints.rs | 27 +++++++++++++------ .../src/types/infer/builder.rs | 21 ++++++--------- 4 files changed, 49 insertions(+), 24 deletions(-) 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 f83bee977c..ec4a31a711 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -126,7 +126,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc ```py from typing import Any, final, Never, Sequence -from ty_extensions import ConstraintSet, static_assert +from ty_extensions import ConstraintSet, Not, static_assert class Super: ... class Base(Super): ... @@ -207,6 +207,15 @@ def _[T]() -> None: static_assert(constraints == expected) ``` +A negated _type_ is not the same thing as a negated _range_. + +```py +def _[T]() -> None: + negated_type = ConstraintSet.range(Never, T, Not[int]) + negated_constraint = ~ConstraintSet.range(Never, T, int) + static_assert(negated_type != negated_constraint) +``` + ## Intersection The intersection of two constraint sets requires that the constraints in both sets hold. In many diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 12a38a8e34..be02d27764 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1268,8 +1268,14 @@ impl<'db> Type<'db> { self.as_union().expect("Expected a Type::Union variant") } - pub(crate) 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 { + match self { + Type::Intersection(intersection) => !intersection.is_simple_negation(db), + _ => false, + } } pub(crate) const fn as_function_literal(self) -> Option> { @@ -14151,6 +14157,10 @@ impl<'db> IntersectionType<'db> { (self.positive(db).len() + self.negative(db).len()) == 1 } + pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool { + self.positive(db).is_empty() && self.negative(db).len() == 1 + } + fn heap_size((positive, negative): &(FxOrderSet>, FxOrderSet>)) -> usize { ruff_memory_usage::order_set_heap_size(positive) + ruff_memory_usage::order_set_heap_size(negative) diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 7a727f3285..289e134324 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -435,6 +435,11 @@ impl<'db> ConstraintSet<'db> { pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { self.node.simplify_for_display(db).display(db) } + + #[expect(dead_code)] // Keep this around for debugging purposes + pub(crate) fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display { + self.node.display_graph(db, prefix) + } } impl From for ConstraintSet<'_> { @@ -498,11 +503,13 @@ impl<'db> ConstrainedTypeVar<'db> { debug_assert_eq!(upper, upper.top_materialization(db)); // It's not useful for an upper bound to be an intersection type, or for a lower bound to - // be a union type. Both of those can be rewritten as simpler BDDs: + // be a union type. Because the following equivalences hold, we can break these bounds + // apart and create an equivalent BDD with more nodes but simpler constraints. (Fewer, + // simpler constraints mean that our sequent maps won't grow pathologically large.) // - // T ≤ α & β ⇒ (T ≤ α) ∧ (T ≤ β) - // T ≤ α & ¬β ⇒ (T ≤ α) ∧ ¬(T ≤ β) - // α | β ≤ T ⇒ (α ≤ T) ∧ (β ≤ T) + // T ≤ (α & β) ⇔ (T ≤ α) ∧ (T ≤ β) + // T ≤ (¬α & ¬β) ⇔ (T ≤ ¬α) ∧ (T ≤ ¬β) + // (α | β) ≤ T ⇔ (α ≤ T) ∧ (β ≤ T) if let Type::Union(lower_union) = lower { let mut result = Node::AlwaysTrue; for lower_element in lower_union.elements(db) { @@ -513,7 +520,12 @@ impl<'db> ConstrainedTypeVar<'db> { } return result; } - if let Type::Intersection(upper_intersection) = upper { + // 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 + // purposes of simplifying upper bounds. + if let Type::Intersection(upper_intersection) = upper + && !upper_intersection.is_simple_negation(db) + { let mut result = Node::AlwaysTrue; for upper_element in upper_intersection.iter_positive(db) { result = result.and( @@ -524,7 +536,7 @@ impl<'db> ConstrainedTypeVar<'db> { for upper_element in upper_intersection.iter_negative(db) { result = result.and( db, - ConstrainedTypeVar::new_node(db, typevar, lower, upper_element).negate(db), + ConstrainedTypeVar::new_node(db, typevar, lower, upper_element.negate(db)), ); } return result; @@ -716,7 +728,7 @@ impl<'db> ConstrainedTypeVar<'db> { return IntersectionResult::Disjoint; } - if lower.is_union() || upper.is_intersection() { + if lower.is_union() || upper.is_nontrivial_intersection(db) { return IntersectionResult::CannotSimplify; } @@ -1579,7 +1591,6 @@ impl<'db> Node<'db> { /// │ └─₀ never /// └─₀ never /// ``` - #[cfg_attr(not(test), expect(dead_code))] // Keep this around for debugging purposes fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display { struct DisplayNode<'a, 'db> { db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 06cb805c15..5c733454a0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10844,19 +10844,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ( Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), - ) => { - let result = match op { - ast::CmpOp::Eq => Some( - left.constraints(self.db()).iff(self.db(), right.constraints(self.db())) - ), - ast::CmpOp::NotEq => Some( - left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).negate(self.db()) - ), - _ => None, - }; - result.map(|constraints| Ok(Type::KnownInstance(KnownInstanceType::ConstraintSet( - TrackedConstraintSet::new(self.db(), constraints) - )))) + ) => match op { + ast::CmpOp::Eq => Some(Ok(Type::BooleanLiteral( + left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).is_always_satisfied(self.db()), + ))), + ast::CmpOp::NotEq => Some(Ok(Type::BooleanLiteral( + !left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).is_always_satisfied(self.db()), + ))), + _ => None, } ( From a2fb2ee06ccd81db6e205b23f717af23f6514659 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 10 Dec 2025 15:15:10 -0500 Subject: [PATCH 13/36] [ty] Fix disjointness checks with type-of `@final` classes (#21770) ## Summary We currently perform a subtyping check, similar to what we were doing for `@final` instances before https://github.com/astral-sh/ruff/pull/21167, which is incorrect, e.g. we currently consider `type[X[Any]]` and `type[X[T]]]` disjoint (where `X` is `@final`). --- .../resources/mdtest/narrow/type.md | 6 +- .../resources/mdtest/type_of/basic.md | 176 +++++++++++++++++- .../type_properties/is_disjoint_from.md | 17 +- crates/ty_python_semantic/src/types.rs | 42 ++++- crates/ty_python_semantic/src/types/class.rs | 9 - 5 files changed, 218 insertions(+), 32 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index 3cf1aa23db..de962d2075 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -92,8 +92,7 @@ def f(x: A[int] | B): reveal_type(x) # revealed: A[int] | B if type(x) is A: - # TODO: this should be `A[int]`, but `A[int] | B` would be better than `Never` - reveal_type(x) # revealed: Never + reveal_type(x) # revealed: A[int] else: reveal_type(x) # revealed: A[int] | B @@ -111,8 +110,7 @@ def f(x: A[int] | B): if type(x) is not A: reveal_type(x) # revealed: A[int] | B else: - # TODO: this should be `A[int]`, but `A[int] | B` would be better than `Never` - reveal_type(x) # revealed: Never + reveal_type(x) # revealed: A[int] if type(x) is not B: reveal_type(x) # revealed: A[int] | B 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 da89cc4c16..6d747cb6f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -160,7 +160,7 @@ same also applies to enum classes with members, which are implicitly final: ```toml [environment] -python-version = "3.10" +python-version = "3.12" ``` ```py @@ -180,3 +180,177 @@ def _(x: type[Foo], y: type[EllipsisType], z: type[Answer]): reveal_type(y) # revealed: reveal_type(z) # revealed: ``` + +## Subtyping `@final` classes + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import final, Any +from ty_extensions import is_assignable_to, is_subtype_of, is_disjoint_from, static_assert + +class Biv[T]: ... + +class Cov[T]: + def pop(self) -> T: + raise NotImplementedError + +class Contra[T]: + def push(self, value: T) -> None: + pass + +class Inv[T]: + x: T + +@final +class BivSub[T](Biv[T]): ... + +@final +class CovSub[T](Cov[T]): ... + +@final +class ContraSub[T](Contra[T]): ... + +@final +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]])) + + static_assert(not is_subtype_of(type[ContraSub[T]], type[ContraSub[U]])) + static_assert(not is_disjoint_from(type[ContraSub[U]], type[ContraSub[T]])) + + static_assert(not is_subtype_of(type[InvSub[T]], type[InvSub[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]])) + # `CovSub[Never]` is a subtype of both `CovSub[int]` and `CovSub[str]`. + static_assert(not is_disjoint_from(type[CovSub[int]], type[CovSub[str]])) + + static_assert(not is_subtype_of(type[ContraSub[bool]], type[ContraSub[int]])) + static_assert(is_subtype_of(type[ContraSub[int]], type[ContraSub[bool]])) + static_assert(not is_disjoint_from(type[ContraSub[bool]], type[ContraSub[int]])) + # `ContraSub[int | str]` is a subtype of both `ContraSub[int]` and `ContraSub[str]`. + static_assert(not is_disjoint_from(type[ContraSub[int]], type[ContraSub[str]])) + + static_assert(not is_subtype_of(type[InvSub[bool]], type[InvSub[int]])) + static_assert(not is_subtype_of(type[InvSub[int]], type[InvSub[bool]])) + static_assert(is_disjoint_from(type[InvSub[int]], type[InvSub[str]])) + # TODO: These are disjoint. + static_assert(not 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]])) + static_assert(is_assignable_to(type[CovSub[Any]], type[CovSub[T]])) + static_assert(not is_disjoint_from(type[CovSub[T]], type[CovSub[Any]])) + + static_assert(not is_subtype_of(type[ContraSub[T]], type[ContraSub[Any]])) + static_assert(not is_subtype_of(type[ContraSub[Any]], type[ContraSub[T]])) + static_assert(is_assignable_to(type[ContraSub[T]], type[ContraSub[Any]])) + static_assert(is_assignable_to(type[ContraSub[Any]], type[ContraSub[T]])) + static_assert(not is_disjoint_from(type[ContraSub[T]], type[ContraSub[Any]])) + + static_assert(not is_subtype_of(type[InvSub[T]], type[InvSub[Any]])) + static_assert(not is_subtype_of(type[InvSub[Any]], type[InvSub[T]])) + static_assert(is_assignable_to(type[InvSub[T]], type[InvSub[Any]])) + static_assert(is_assignable_to(type[InvSub[Any]], type[InvSub[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]])) + static_assert(not is_disjoint_from(type[CovSub[U]], type[Cov[T]])) + static_assert(not is_disjoint_from(type[CovSub[U]], type[Cov[U]])) + + static_assert(is_subtype_of(type[ContraSub[T]], type[Contra[T]])) + static_assert(not is_subtype_of(type[Contra[T]], type[ContraSub[T]])) + static_assert(not is_disjoint_from(type[ContraSub[T]], type[Contra[T]])) + static_assert(not is_disjoint_from(type[ContraSub[U]], type[Contra[T]])) + static_assert(not is_disjoint_from(type[ContraSub[U]], type[Contra[U]])) + + static_assert(is_subtype_of(type[InvSub[T]], type[Inv[T]])) + static_assert(not is_subtype_of(type[Inv[T]], type[InvSub[T]])) + static_assert(not is_disjoint_from(type[InvSub[T]], type[Inv[T]])) + static_assert(not is_disjoint_from(type[InvSub[U]], type[Inv[T]])) + 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]])) + static_assert(not is_disjoint_from(type[CovSub[int]], type[Cov[bool]])) + + static_assert(not is_subtype_of(type[ContraSub[bool]], type[Contra[int]])) + static_assert(is_subtype_of(type[ContraSub[int]], type[Contra[bool]])) + static_assert(not is_disjoint_from(type[ContraSub[int]], type[Contra[bool]])) + static_assert(not is_disjoint_from(type[ContraSub[bool]], type[Contra[int]])) + + static_assert(not is_subtype_of(type[InvSub[bool]], type[Inv[int]])) + static_assert(not is_subtype_of(type[InvSub[int]], type[Inv[bool]])) + # TODO: These are disjoint. + static_assert(not is_disjoint_from(type[InvSub[bool]], type[Inv[int]])) + # TODO: These are disjoint. + static_assert(not 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]])) + static_assert(is_assignable_to(type[CovSub[Any]], type[Cov[T]])) + static_assert(not is_disjoint_from(type[CovSub[T]], type[Cov[Any]])) + + static_assert(not is_subtype_of(type[ContraSub[T]], type[Contra[Any]])) + static_assert(not is_subtype_of(type[ContraSub[Any]], type[Contra[T]])) + static_assert(is_assignable_to(type[ContraSub[T]], type[Contra[Any]])) + static_assert(is_assignable_to(type[ContraSub[Any]], type[Contra[T]])) + static_assert(not is_disjoint_from(type[ContraSub[T]], type[Contra[Any]])) + + static_assert(not is_subtype_of(type[InvSub[T]], type[Inv[Any]])) + static_assert(not is_subtype_of(type[InvSub[Any]], type[Inv[T]])) + static_assert(is_assignable_to(type[InvSub[T]], type[Inv[Any]])) + static_assert(is_assignable_to(type[InvSub[Any]], type[Inv[T]])) + static_assert(not is_disjoint_from(type[InvSub[T]], type[Inv[Any]])) +``` 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 02f50cc412..d4aa7db231 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 @@ -684,9 +684,8 @@ class GenericClass[T]: x: T # invariant static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass])) -# TODO: these should not error -static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass])) # error: [static-assert-error] -static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass])) +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]])) @@ -694,19 +693,17 @@ class GenericClassIntBound[T: int]: x: T # invariant static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound])) -# TODO: these should not error -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound])) # error: [static-assert-error] -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) # error: [static-assert-error] +static_assert(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 -# TODO: these should not error -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass])) # error: [static-assert-error] -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) # error: [static-assert-error] -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) # error: [static-assert-error] +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) +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.rs b/crates/ty_python_semantic/src/types.rs index be02d27764..796a3e6962 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3353,7 +3353,6 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) - | Type::GenericAlias(..) | Type::SpecialForm(..) | Type::KnownInstance(..)), right @ (Type::BooleanLiteral(..) @@ -3367,7 +3366,6 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) - | Type::GenericAlias(..) | Type::SpecialForm(..) | Type::KnownInstance(..)), ) => ConstraintSet::from(left != right), @@ -3550,13 +3548,39 @@ impl<'db> Type<'db> { ConstraintSet::from(true) } + (Type::GenericAlias(left_alias), Type::GenericAlias(right_alias)) => { + ConstraintSet::from(left_alias.origin(db) != right_alias.origin(db)).or(db, || { + left_alias.specialization(db).is_disjoint_from_impl( + db, + right_alias.specialization(db), + inferable, + disjointness_visitor, + relation_visitor, + ) + }) + } + + (Type::ClassLiteral(class_literal), other @ Type::GenericAlias(_)) + | (other @ Type::GenericAlias(_), Type::ClassLiteral(class_literal)) => class_literal + .default_specialization(db) + .into_generic_alias() + .when_none_or(|alias| { + other.is_disjoint_from_impl( + db, + Type::GenericAlias(alias), + inferable, + disjointness_visitor, + relation_visitor, + ) + }), + (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(_) => ConstraintSet::from(false), - SubclassOfInner::Class(class_a) => { - class_b.when_subclass_of(db, None, class_a).negate(db) - } + SubclassOfInner::Class(class_a) => ConstraintSet::from( + !class_a.could_exist_in_mro_of(db, ClassType::NonGeneric(class_b)), + ), SubclassOfInner::TypeVar(_) => unreachable!(), } } @@ -3565,9 +3589,9 @@ impl<'db> Type<'db> { | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => ConstraintSet::from(false), - SubclassOfInner::Class(class_a) => ClassType::from(alias_b) - .when_subclass_of(db, class_a, inferable) - .negate(db), + SubclassOfInner::Class(class_a) => ConstraintSet::from( + !class_a.could_exist_in_mro_of(db, ClassType::Generic(alias_b)), + ), SubclassOfInner::TypeVar(_) => unreachable!(), } } @@ -3861,6 +3885,8 @@ impl<'db> Type<'db> { relation_visitor, ) } + + (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => ConstraintSet::from(true), } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 492ed63f21..71ae73f1b1 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1911,15 +1911,6 @@ impl<'db> ClassLiteral<'db> { .contains(&ClassBase::Class(other)) } - pub(super) fn when_subclass_of( - self, - db: &'db dyn Db, - specialization: Option>, - other: ClassType<'db>, - ) -> ConstraintSet<'db> { - ConstraintSet::from(self.is_subclass_of(db, specialization, other)) - } - /// Return `true` if this class constitutes a typed dict specification (inherits from /// `typing.TypedDict`, either directly or indirectly). #[salsa::tracked(cycle_initial=is_typed_dict_cycle_initial, From 1b44d7e2a7eb80cf22dd2e2612c1c6fc8756369c Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Wed, 10 Dec 2025 12:36:36 -0800 Subject: [PATCH 14/36] [ty] add `SyntheticTypedDictType` and implement `normalized` and `is_equivalent_to` (#21784) --- crates/ty/docs/rules.md | 148 ++++---- ...undant_cast_warni…_(75ac240a2d1f7108).snap | 55 +++ .../resources/mdtest/typed_dict.md | 166 +++++++++ crates/ty_python_semantic/src/types.rs | 22 +- .../src/types/diagnostic.rs | 7 +- .../ty_python_semantic/src/types/display.rs | 56 +++- .../ty_python_semantic/src/types/function.rs | 24 +- .../src/types/subclass_of.rs | 9 +- .../src/types/typed_dict.rs | 316 ++++++++++++++++-- 9 files changed, 676 insertions(+), 127 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni…_(75ac240a2d1f7108).snap diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 5ac36c4fb9..e32e8d3e53 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -826,7 +826,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -934,7 +934,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1095,7 +1095,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1125,7 +1125,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1175,7 +1175,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1201,7 +1201,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1232,7 +1232,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1266,7 +1266,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 @@ -1315,7 +1315,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1340,7 +1340,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1398,7 +1398,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1425,7 +1425,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1472,7 +1472,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1502,7 +1502,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1532,7 +1532,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1566,7 +1566,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1600,7 +1600,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1635,7 +1635,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1660,7 +1660,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1693,7 +1693,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1722,7 +1722,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1746,7 +1746,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1772,7 +1772,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1805,7 +1805,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1832,7 +1832,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1890,7 +1890,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1920,7 +1920,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1949,7 +1949,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -1983,7 +1983,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2010,7 +2010,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2038,7 +2038,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2084,7 +2084,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2111,7 +2111,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2139,7 +2139,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2164,7 +2164,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2189,7 +2189,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2226,7 +2226,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2254,7 +2254,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2279,7 +2279,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2320,7 +2320,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2408,7 +2408,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2436,7 +2436,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2468,7 +2468,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2500,7 +2500,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2527,7 +2527,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2551,7 +2551,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2609,7 +2609,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2648,7 +2648,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2711,7 +2711,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2735,7 +2735,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni…_(75ac240a2d1f7108).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni…_(75ac240a2d1f7108).snap new file mode 100644 index 0000000000..16f2235fc9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni…_(75ac240a2d1f7108).snap @@ -0,0 +1,55 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: typed_dict.md - `TypedDict` - Redundant cast warnings +mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypedDict, cast + 2 | + 3 | class Foo2(TypedDict): + 4 | x: int + 5 | + 6 | class Bar2(TypedDict): + 7 | x: int + 8 | + 9 | foo: Foo2 = {"x": 1} +10 | _ = cast(Foo2, foo) # error: [redundant-cast] +11 | _ = cast(Bar2, foo) # error: [redundant-cast] +``` + +# Diagnostics + +``` +warning[redundant-cast]: Value is already of type `Foo2` + --> src/mdtest_snippet.py:10:5 + | + 9 | foo: Foo2 = {"x": 1} +10 | _ = cast(Foo2, foo) # error: [redundant-cast] + | ^^^^^^^^^^^^^^^ +11 | _ = cast(Bar2, foo) # error: [redundant-cast] + | +info: rule `redundant-cast` is enabled by default + +``` + +``` +warning[redundant-cast]: Value is already of type `Bar2` + --> src/mdtest_snippet.py:11:5 + | + 9 | foo: Foo2 = {"x": 1} +10 | _ = cast(Foo2, foo) # error: [redundant-cast] +11 | _ = cast(Bar2, foo) # error: [redundant-cast] + | ^^^^^^^^^^^^^^^ + | +info: `Bar2` is equivalent to `Foo2` +info: rule `redundant-cast` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index aadf8249ae..ad15f28250 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -868,6 +868,172 @@ def _(o1: Outer1, o2: Outer2, o3: Outer3, o4: Outer4): static_assert(is_subtype_of(Outer4, Outer4)) ``` +## Structural equivalence + +Two `TypedDict`s with equivalent fields are equivalent types. This includes fields with gradual +types: + +```py +from typing_extensions import Any, TypedDict, ReadOnly, assert_type +from ty_extensions import is_assignable_to, is_equivalent_to, static_assert + +class Foo(TypedDict): + x: int + y: Any + +# exactly the same fields +class Bar(TypedDict): + x: int + y: Any + +# the same fields but in a different order +class Baz(TypedDict): + y: Any + x: int + +static_assert(is_assignable_to(Foo, Bar)) +static_assert(is_equivalent_to(Foo, Bar)) +static_assert(is_assignable_to(Foo, Baz)) +static_assert(is_equivalent_to(Foo, Baz)) + +foo: Foo = {"x": 1, "y": "hello"} +assert_type(foo, Foo) +assert_type(foo, Bar) +assert_type(foo, Baz) +``` + +Equivalent `TypedDict`s within unions can also produce equivalent unions, which currently relies on +"normalization" machinery: + +```py +def f(var: Foo | int): + assert_type(var, Foo | int) + assert_type(var, Bar | int) + assert_type(var, Baz | int) + # TODO: Union simplification compares `TypedDict`s by name/identity to avoid cycles. This assert + # should also pass once that's fixed. + assert_type(var, Foo | Bar | Baz | int) # error: [type-assertion-failure] +``` + +Here are several cases that are not equivalent. In particular, assignability does not imply +equivalence: + +```py +class FewerFields(TypedDict): + x: int + +static_assert(is_assignable_to(Foo, FewerFields)) +static_assert(not is_equivalent_to(Foo, FewerFields)) + +class DifferentMutability(TypedDict): + x: int + y: ReadOnly[Any] + +static_assert(is_assignable_to(Foo, DifferentMutability)) +static_assert(not is_equivalent_to(Foo, DifferentMutability)) + +class MoreFields(TypedDict): + x: int + y: Any + z: str + +static_assert(not is_assignable_to(Foo, MoreFields)) +static_assert(not is_equivalent_to(Foo, MoreFields)) + +class DifferentFieldStaticType(TypedDict): + x: str + y: Any + +static_assert(not is_assignable_to(Foo, DifferentFieldStaticType)) +static_assert(not is_equivalent_to(Foo, DifferentFieldStaticType)) + +class DifferentFieldGradualType(TypedDict): + x: int + y: Any | str + +static_assert(is_assignable_to(Foo, DifferentFieldGradualType)) +static_assert(not is_equivalent_to(Foo, DifferentFieldGradualType)) +``` + +## Structural equivalence understands the interaction between `Required`/`NotRequired` and `total` + +```py +from ty_extensions import static_assert, is_equivalent_to +from typing_extensions import TypedDict, Required, NotRequired + +class Foo1(TypedDict, total=False): + x: int + y: str + +class Foo2(TypedDict): + y: NotRequired[str] + x: NotRequired[int] + +static_assert(is_equivalent_to(Foo1, Foo2)) +static_assert(is_equivalent_to(Foo1 | int, int | Foo2)) + +class Bar1(TypedDict, total=False): + x: int + y: Required[str] + +class Bar2(TypedDict): + y: str + x: NotRequired[int] + +static_assert(is_equivalent_to(Bar1, Bar2)) +static_assert(is_equivalent_to(Bar1 | int, int | Bar2)) +``` + +## Assignability and equivalence work with recursive `TypedDict`s + +```py +from typing_extensions import TypedDict +from ty_extensions import static_assert, is_assignable_to, is_equivalent_to + +class Node1(TypedDict): + value: int + next: "Node1" | None + +class Node2(TypedDict): + value: int + next: "Node2" | None + +static_assert(is_assignable_to(Node1, Node2)) +static_assert(is_equivalent_to(Node1, Node2)) + +class Person1(TypedDict): + name: str + friends: list["Person1"] + +class Person2(TypedDict): + name: str + friends: list["Person2"] + +static_assert(is_assignable_to(Person1, Person2)) +static_assert(is_equivalent_to(Person1, Person2)) +``` + +## Redundant cast warnings + + + +Casting between equivalent types produces a redundant cast warning. When the types have different +names, the warning makes that clear: + +```py +from typing import TypedDict, cast + +class Foo2(TypedDict): + x: int + +class Bar2(TypedDict): + x: int + +foo: Foo2 = {"x": 1} +_ = cast(Foo2, foo) # error: [redundant-cast] +_ = cast(Bar2, foo) # error: [redundant-cast] +``` + ## Key-based access ### Reading diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 796a3e6962..1ecb4504b6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1471,6 +1471,7 @@ impl<'db> Type<'db> { /// - Strips the types of default values from parameters in `Callable` types: only whether a parameter /// *has* or *does not have* a default value is relevant to whether two `Callable` types are equivalent. /// - Converts class-based protocols into synthesized protocols + /// - Converts class-based typeddicts into synthesized typeddicts #[must_use] pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { self.normalized_impl(db, &NormalizedVisitor::default()) @@ -1529,10 +1530,9 @@ impl<'db> Type<'db> { // Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`) enum_literal.enum_class_instance(db) } - Type::TypedDict(_) => { - // TODO: Normalize TypedDicts - self - } + Type::TypedDict(typed_dict) => visitor.visit(self, || { + Type::TypedDict(typed_dict.normalized_impl(db, visitor)) + }), Type::TypeAlias(alias) => alias.value_type(db).normalized_impl(db, visitor), Type::NewTypeInstance(newtype) => { visitor.visit(self, || { @@ -3053,6 +3053,10 @@ impl<'db> Type<'db> { left.is_equivalent_to_impl(db, right, inferable, visitor) } + (Type::TypedDict(left), Type::TypedDict(right)) => visitor.visit((self, other), || { + left.is_equivalent_to_impl(db, right, inferable, visitor) + }), + _ => ConstraintSet::from(false), } } @@ -7582,7 +7586,13 @@ impl<'db> Type<'db> { Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), // `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) => SubclassOfType::from(db, typed_dict.defining_class()), + Type::TypedDict(typed_dict) => match typed_dict { + TypedDictType::Class(class) => SubclassOfType::from(db, class), + TypedDictType::Synthesized(_) => SubclassOfType::from( + db, + todo_type!("TypedDict synthesized meta-type").expect_dynamic(), + ), + }, Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db), Type::NewTypeInstance(newtype) => Type::from(newtype.base_class_type(db)), } @@ -8291,7 +8301,7 @@ impl<'db> Type<'db> { }, Self::TypedDict(typed_dict) => { - Some(TypeDefinition::Class(typed_dict.defining_class().definition(db))) + typed_dict.definition(db).map(TypeDefinition::Class) } Self::Union(_) | Self::Intersection(_) => None, diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 2706787998..23bb5b1a16 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -14,9 +14,7 @@ use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; use crate::semantic_index::{global_scope, place_table, use_def_map}; use crate::suppression::FileSuppressionId; use crate::types::call::CallError; -use crate::types::class::{ - CodeGeneratorKind, DisjointBase, DisjointBaseKind, Field, MethodDecorator, -}; +use crate::types::class::{CodeGeneratorKind, DisjointBase, DisjointBaseKind, MethodDecorator}; use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral}; use crate::types::infer::UnsupportedComparisonError; use crate::types::overrides::MethodKind; @@ -26,6 +24,7 @@ use crate::types::string_annotation::{ RAW_STRING_TYPE_ANNOTATION, }; use crate::types::tuple::TupleSpec; +use crate::types::typed_dict::TypedDictSchema; use crate::types::{ BoundTypeVarInstance, ClassType, DynamicType, LintDiagnosticGuard, Protocol, ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, @@ -3471,7 +3470,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( typed_dict_ty: Type<'db>, full_object_ty: Option>, key_ty: Type<'db>, - items: &FxIndexMap>, + items: &TypedDictSchema<'db>, ) { let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_KEY, key_node) { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 19ed71bbff..39e5a1caee 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -25,8 +25,8 @@ use crate::types::visitor::TypeVisitor; use crate::types::{ BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, Protocol, ProtocolInstanceType, - SpecialFormType, StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind, - visitor, + SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypedDictType, UnionType, + WrapperDescriptorKind, visitor, }; /// Settings for displaying types and signatures @@ -900,12 +900,24 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } f.write_str("]") } - Type::TypedDict(typed_dict) => typed_dict - .defining_class() + Type::TypedDict(TypedDictType::Class(defining_class)) => defining_class .class_literal(self.db) .0 .display_with(self.db, self.settings.clone()) .fmt_detailed(f), + Type::TypedDict(TypedDictType::Synthesized(synthesized)) => { + f.set_invalid_syntax(); + f.write_str("') + } Type::TypeAlias(alias) => { f.write_str(alias.name(self.db))?; match alias.specialization(self.db) { @@ -2373,7 +2385,10 @@ mod tests { use crate::Db; use crate::db::tests::setup_db; use crate::place::typing_extensions_symbol; - use crate::types::{KnownClass, Parameter, Parameters, Signature, Type}; + use crate::types::typed_dict::{ + SynthesizedTypedDictType, TypedDictFieldBuilder, TypedDictSchema, + }; + use crate::types::{KnownClass, Parameter, Parameters, Signature, Type, TypedDictType}; #[test] fn string_literal_display() { @@ -2418,6 +2433,37 @@ mod tests { ); } + #[test] + fn synthesized_typeddict_display() { + let db = setup_db(); + + let mut items = TypedDictSchema::default(); + items.insert( + Name::new("foo"), + TypedDictFieldBuilder::new(Type::IntLiteral(42)) + .required(true) + .build(), + ); + items.insert( + Name::new("bar"), + TypedDictFieldBuilder::new(Type::string_literal(&db, "hello")) + .required(true) + .build(), + ); + + let synthesized = SynthesizedTypedDictType::new(&db, items); + let type_ = Type::TypedDict(TypedDictType::Synthesized(synthesized)); + // Fields are sorted internally, even prior to normalization. + assert_eq!( + type_.display(&db).to_string(), + "", + ); + assert_eq!( + type_.normalized(&db).display(&db).to_string(), + "", + ); + } + fn display_signature<'db>( db: &'db dyn Db, parameters: impl IntoIterator>, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index dae46bca03..cc2c358590 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1028,18 +1028,6 @@ impl<'db> FunctionType<'db> { relation_visitor: &HasRelationToVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>, ) -> ConstraintSet<'db> { - // A function type is the subtype of itself, and not of any other function type. However, - // our representation of a function type includes any specialization that should be applied - // to the signature. Different specializations of the same function type are only subtypes - // of each other if they result in subtype signatures. - if matches!( - relation, - TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_) - ) && self.normalized(db) == other.normalized(db) - { - return ConstraintSet::from(true); - } - if self.literal(db) != other.literal(db) { return ConstraintSet::from(false); } @@ -1621,10 +1609,16 @@ impl KnownFunction { && !any_over_type(db, *casted_type, &contains_unknown_or_todo, true) { if let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression) { - builder.into_diagnostic(format_args!( - "Value is already of type `{}`", - casted_type.display(db), + let source_display = source_type.display(db).to_string(); + let casted_display = casted_type.display(db).to_string(); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Value is already of type `{casted_display}`", )); + if source_display != casted_display { + diagnostic.info(format_args!( + "`{casted_display}` is equivalent to `{source_display}`", + )); + } } } } diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 898a82e086..c6bb9d0378 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -8,7 +8,7 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, NormalizedVisitor, SpecialFormType, Type, TypeContext, - TypeMapping, TypeRelation, TypeVarBoundOrConstraints, todo_type, + TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypedDictType, todo_type, }; use crate::{Db, FxOrderSet}; @@ -381,7 +381,12 @@ impl<'db> SubclassOfInner<'db> { pub(crate) 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) => SubclassOfInner::Class(typed_dict.defining_class()), + Type::TypedDict(typed_dict) => match typed_dict { + TypedDictType::Class(class) => SubclassOfInner::Class(class), + TypedDictType::Synthesized(_) => SubclassOfInner::Dynamic( + todo_type!("type[T] for synthesized TypedDicts").expect_dynamic(), + ), + }, Type::TypeVar(bound_typevar) => SubclassOfInner::TypeVar(bound_typevar), Type::Dynamic(DynamicType::Any) => SubclassOfInner::Dynamic(DynamicType::Any), Type::Dynamic(DynamicType::Unknown) => SubclassOfInner::Dynamic(DynamicType::Unknown), diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 14ee100d2d..e07fbb999d 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -1,3 +1,6 @@ +use std::collections::BTreeMap; +use std::ops::{Deref, DerefMut}; + use bitflags::bitflags; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; use ruff_db::parsed::parsed_module; @@ -12,10 +15,15 @@ use super::diagnostic::{ report_missing_typed_dict_key, }; use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor}; -use crate::types::constraints::ConstraintSet; +use crate::Db; +use crate::semantic_index::definition::Definition; +use crate::types::class::FieldKind; +use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::InferableTypeVars; -use crate::types::{HasRelationToVisitor, IsDisjointVisitor, TypeContext, TypeRelation}; -use crate::{Db, FxIndexMap}; +use crate::types::{ + HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, TypeContext, + TypeRelation, +}; use ordermap::OrderSet; @@ -41,24 +49,60 @@ impl Default for TypedDictParams { /// Type that represents the set of all inhabitants (`dict` instances) that conform to /// a given `TypedDict` schema. #[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)] -pub struct TypedDictType<'db> { +pub enum TypedDictType<'db> { /// A reference to the class (inheriting from `typing.TypedDict`) that specifies the /// schema of this `TypedDict`. - defining_class: ClassType<'db>, + Class(ClassType<'db>), + /// A `TypedDict` that doesn't correspond to a class definition, either because it's been + /// `normalized`, or because it's been synthesized to represent constraints. + Synthesized(SynthesizedTypedDictType<'db>), } impl<'db> TypedDictType<'db> { pub(crate) fn new(defining_class: ClassType<'db>) -> Self { - Self { defining_class } + Self::Class(defining_class) } - pub(crate) fn defining_class(self) -> ClassType<'db> { - self.defining_class + pub(crate) fn defining_class(self) -> Option> { + match self { + Self::Class(defining_class) => Some(defining_class), + Self::Synthesized(_) => None, + } } - pub(crate) fn items(self, db: &'db dyn Db) -> &'db FxIndexMap> { - let (class_literal, specialization) = self.defining_class.class_literal(db); - class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict) + pub(crate) fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> { + #[salsa::tracked(returns(ref))] + fn class_based_items<'db>(db: &'db dyn Db, class: ClassType<'db>) -> TypedDictSchema<'db> { + let (class_literal, specialization) = class.class_literal(db); + class_literal + .fields(db, specialization, CodeGeneratorKind::TypedDict) + .into_iter() + .map(|(name, field)| { + let field = match field { + Field { + first_declaration, + declared_ty, + kind: + FieldKind::TypedDict { + is_required, + is_read_only, + }, + } => TypedDictFieldBuilder::new(*declared_ty) + .required(*is_required) + .read_only(*is_read_only) + .first_declaration(*first_declaration) + .build(), + _ => unreachable!("TypedDict field expected"), + }; + (name.clone(), field) + }) + .collect() + } + + match self { + Self::Class(defining_class) => class_based_items(db, defining_class), + Self::Synthesized(synthesized) => synthesized.items(db), + } } pub(crate) fn apply_type_mapping_impl<'a>( @@ -69,12 +113,12 @@ impl<'db> TypedDictType<'db> { visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { // TODO: Materialization of gradual TypedDicts needs more logic - Self { - defining_class: self.defining_class.apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, + match self { + Self::Class(defining_class) => { + Self::Class(defining_class.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + } + Self::Synthesized(synthesized) => Self::Synthesized( + synthesized.apply_type_mapping_impl(db, type_mapping, tcx, visitor), ), } } @@ -93,9 +137,9 @@ impl<'db> TypedDictType<'db> { // First do a quick nominal check that (if it succeeds) means that we can avoid // materializing the full `TypedDict` schema for either `self` or `target`. // This should be cheaper in many cases, and also helps us avoid some cycles. - if self - .defining_class - .is_subclass_of(db, target.defining_class) + if let Some(defining_class) = self.defining_class() + && let Some(target_defining_class) = target.defining_class() + && defining_class.is_subclass_of(db, target_defining_class) { return ConstraintSet::from(true); } @@ -246,6 +290,57 @@ impl<'db> TypedDictType<'db> { } constraints } + + pub fn definition(self, db: &'db dyn Db) -> Option> { + match self { + TypedDictType::Class(defining_class) => Some(defining_class.definition(db)), + TypedDictType::Synthesized(_) => None, + } + } + + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + match self { + TypedDictType::Class(_) => { + let synthesized = SynthesizedTypedDictType::new(db, self.items(db)); + TypedDictType::Synthesized(synthesized.normalized_impl(db, visitor)) + } + TypedDictType::Synthesized(synthesized) => { + TypedDictType::Synthesized(synthesized.normalized_impl(db, visitor)) + } + } + } + + pub(crate) fn is_equivalent_to_impl( + self, + db: &'db dyn Db, + other: TypedDictType<'db>, + inferable: InferableTypeVars<'_, 'db>, + visitor: &IsEquivalentVisitor<'db>, + ) -> ConstraintSet<'db> { + // TODO: `closed` and `extra_items` support will go here. Until then we don't look at the + // params at all, because `total` is already incorporated into `FieldKind`. + + // Since both sides' fields are pre-sorted into `BTreeMap`s, we can iterate over them in + // sorted order instead of paying for a lookup for each field, as long as their lengths are + // the same. + if self.items(db).len() != other.items(db).len() { + return ConstraintSet::from(false); + } + self.items(db).iter().zip(other.items(db)).when_all( + db, + |((name, field), (other_name, other_field))| { + if name != other_name || field.flags != other_field.flags { + return ConstraintSet::from(false); + } + field.declared_ty.is_equivalent_to_impl( + db, + other_field.declared_ty, + inferable, + visitor, + ) + }, + ) + } } pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -253,7 +348,16 @@ pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( typed_dict: TypedDictType<'db>, visitor: &V, ) { - visitor.visit_type(db, typed_dict.defining_class.into()); + 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); + } + } + } } pub(super) fn typed_dict_params_from_class_def(class_stmt: &StmtClassDef) -> TypedDictParams { @@ -631,3 +735,173 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( Err(provided_keys) } } + +#[salsa::interned(debug)] +pub struct SynthesizedTypedDictType<'db> { + #[returns(ref)] + pub(crate) items: TypedDictSchema<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for SynthesizedTypedDictType<'_> {} + +impl<'db> SynthesizedTypedDictType<'db> { + pub(super) fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + let items = self + .items(db) + .iter() + .map(|(name, field)| { + let field = field + .clone() + .apply_type_mapping_impl(db, type_mapping, tcx, visitor); + + (name.clone(), field) + }) + .collect::>(); + + SynthesizedTypedDictType::new(db, items) + } + + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + let items = self + .items(db) + .iter() + .map(|(name, field)| { + let field = field.clone().normalized_impl(db, visitor); + (name.clone(), field) + }) + .collect::>(); + Self::new(db, items) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, get_size2::GetSize, salsa::Update)] +pub struct TypedDictSchema<'db>(BTreeMap>); + +impl<'db> Deref for TypedDictSchema<'db> { + type Target = BTreeMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TypedDictSchema<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> IntoIterator for &'a TypedDictSchema<'_> { + type Item = (&'a Name, &'a TypedDictField<'a>); + type IntoIter = std::collections::btree_map::Iter<'a, Name, TypedDictField<'a>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'db> FromIterator<(Name, TypedDictField<'db>)> for TypedDictSchema<'db> { + fn from_iter)>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)] +pub struct TypedDictField<'db> { + pub(super) declared_ty: Type<'db>, + flags: TypedDictFieldFlags, + first_declaration: Option>, +} + +impl<'db> TypedDictField<'db> { + pub(crate) const fn is_required(&self) -> bool { + self.flags.contains(TypedDictFieldFlags::REQUIRED) + } + + pub(crate) const fn is_read_only(&self) -> bool { + self.flags.contains(TypedDictFieldFlags::READ_ONLY) + } + + pub(crate) fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + Self { + declared_ty: self + .declared_ty + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + flags: self.flags, + first_declaration: self.first_declaration, + } + } + + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + Self { + declared_ty: self.declared_ty.normalized_impl(db, visitor), + flags: self.flags, + // A normalized typed-dict field does not hold onto the original declaration, + // since a normalized typed-dict is an abstract type where equality does not depend + // on the source-code definition. + first_declaration: None, + } + } +} + +pub(super) struct TypedDictFieldBuilder<'db> { + declared_ty: Type<'db>, + flags: TypedDictFieldFlags, + first_declaration: Option>, +} + +impl<'db> TypedDictFieldBuilder<'db> { + pub(crate) fn new(declared_ty: Type<'db>) -> Self { + Self { + declared_ty, + flags: TypedDictFieldFlags::empty(), + first_declaration: None, + } + } + + pub(crate) fn required(mut self, yes: bool) -> Self { + self.flags.set(TypedDictFieldFlags::REQUIRED, yes); + self + } + + pub(crate) fn read_only(mut self, yes: bool) -> Self { + self.flags.set(TypedDictFieldFlags::READ_ONLY, yes); + self + } + + pub(crate) fn first_declaration(mut self, definition: Option>) -> Self { + self.first_declaration = definition; + self + } + + pub(crate) fn build(self) -> TypedDictField<'db> { + TypedDictField { + declared_ty: self.declared_ty, + flags: self.flags, + first_declaration: self.first_declaration, + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] + struct TypedDictFieldFlags: u8 { + const REQUIRED = 1 << 0; + const READ_ONLY = 1 << 1; + } +} + +impl get_size2::GetSize for TypedDictFieldFlags {} From 29bf2cd2015b2d5b30a039ddcaea6e95e9ffaedd Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 10 Dec 2025 16:56:20 -0500 Subject: [PATCH 15/36] [ty] Support implicit type of `cls` in signatures (#21771) ## Summary Extends https://github.com/astral-sh/ruff/pull/20517 to support the implicit type of `cls` in `@classmethod` signatures. Part of https://github.com/astral-sh/ty/issues/159. --- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/named_tuple.md | 3 +- .../src/types/signatures.rs | 44 ++++++++++++------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 054f6d6a6a..0536ded1e6 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -607,7 +607,7 @@ class X: def __init__(self, val: int): ... def make_another(self) -> Self: reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__ - return self.__new__(X) + return self.__new__(type(self)) ``` ## Builtin functions and methods diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index ab1dafa187..5fd71b988b 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -271,8 +271,7 @@ reveal_type(Person._make) # revealed: bound method ._make(itera reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any] reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace -# TODO: should be `Person` once we support implicit type of `self` -reveal_type(Person._make(("Alice", 42))) # revealed: Unknown +reveal_type(Person._make(("Alice", 42))) # revealed: Person person = Person("Alice", 42) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 028087ceff..9f8d7ccacd 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -18,8 +18,8 @@ use ruff_python_ast::ParameterWithDefault; use smallvec::{SmallVec, smallvec_inline}; use super::{ - DynamicType, Type, TypeVarVariance, definition_expression_type, infer_definition_types, - semantic_index, + ClassType, DynamicType, Type, TypeVarVariance, definition_expression_type, + infer_definition_types, semantic_index, }; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; @@ -31,8 +31,8 @@ use crate::types::infer::nearest_enclosing_class; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, - KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, - TypeMapping, TypeRelation, VarianceInferable, todo_type, + KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, SubclassOfInner, + SubclassOfType, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -1675,8 +1675,8 @@ impl<'db> Parameters<'db> { }; let method_info = infer_method_information(db, definition); - let is_static_or_classmethod = - method_info.is_some_and(|f| f.is_staticmethod || f.is_classmethod); + let is_staticmethod = method_info.is_some_and(|f| f.is_staticmethod); + let is_classmethod = method_info.is_some_and(|f| f.is_classmethod); let inferred_annotation = |arg: &ParameterWithDefault| { if let Some(MethodInformation { @@ -1685,7 +1685,7 @@ impl<'db> Parameters<'db> { class_is_generic, .. }) = method_info - && !is_static_or_classmethod + && !is_staticmethod && arg.parameter.annotation().is_none() && parameters.index(arg.name().id()) == Some(0) { @@ -1700,16 +1700,30 @@ impl<'db> Parameters<'db> { let index = semantic_index(db, scope_id.file(db)); let class = nearest_enclosing_class(db, index, scope_id).unwrap(); - Some( - typing_self(db, scope_id, typevar_binding_context, class) - .map(Type::TypeVar) - .expect("We should always find the surrounding class for an implicit self: Self annotation"), - ) + let typing_self = typing_self(db, scope_id, typevar_binding_context, class) + .expect("We should always find the surrounding class for an implicit self: Self annotation"); + + if is_classmethod { + Some(SubclassOfType::from( + db, + SubclassOfInner::TypeVar(typing_self), + )) + } else { + Some(Type::TypeVar(typing_self)) + } } else { // For methods of non-generic classes that are not otherwise generic (e.g. return `Self` or - // have additional type parameters), the implicit `Self` type of the `self` parameter would - // be the only type variable, so we can just use the class directly. - Some(class_literal.to_non_generic_instance(db)) + // have additional type parameters), the implicit `Self` type of the `self`, or the implicit + // `type[Self]` type of the `cls` parameter, would be the only type variable, so we can just + // use the class directly. + if is_classmethod { + Some(SubclassOfType::from( + db, + SubclassOfInner::Class(ClassType::NonGeneric(class_literal)), + )) + } else { + Some(class_literal.to_non_generic_instance(db)) + } } } else { None From 2d0681da082b6678f342d768b2c84cac15d829e9 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 10 Dec 2025 18:34:00 -0800 Subject: [PATCH 16/36] [ty] fix missing heap_size on Salsa query (#21912) --- crates/ty_python_semantic/src/types/typed_dict.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e07fbb999d..dee49a3afe 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -71,7 +71,7 @@ impl<'db> TypedDictType<'db> { } pub(crate) fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> { - #[salsa::tracked(returns(ref))] + #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] fn class_based_items<'db>(db: &'db dyn Db, class: ClassType<'db>) -> TypedDictSchema<'db> { let (class_literal, specialization) = class.class_literal(db); class_literal From 24ed28e31434106e3d0bcc8d1a1ede50b645c5da Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 11 Dec 2025 12:28:45 +0530 Subject: [PATCH 17/36] [ty] Improve overload call resolution tracing (#21913) This PR improves the overload call resolution tracing messages as: - Use `trace` level instead of `debug` level - Add a `trace_span` which contains the call arguments and signature - Remove the signature from individual tracing messages --- .../src/types/call/arguments.rs | 47 +++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 31 ++++++------ 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index f3097bb66d..ae377785b8 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::fmt::Display; use itertools::{Either, Itertools}; use ruff_python_ast as ast; @@ -263,6 +264,52 @@ impl<'a, 'db> CallArguments<'a, 'db> { State::Expanding(ExpandingState::Expanded(expanded)) => Expansion::Expanded(expanded), }) } + + pub(super) fn display(&self, db: &'db dyn Db) -> impl Display { + struct DisplayCallArguments<'a, 'db> { + call_arguments: &'a CallArguments<'a, 'db>, + db: &'db dyn Db, + } + + impl std::fmt::Display for DisplayCallArguments<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("(")?; + for (index, (argument, ty)) in self.call_arguments.iter().enumerate() { + if index > 0 { + write!(f, ", ")?; + } + match argument { + Argument::Synthetic => write!( + f, + "self: {}", + ty.unwrap_or_else(Type::unknown).display(self.db) + )?, + Argument::Positional => { + write!(f, "{}", ty.unwrap_or_else(Type::unknown).display(self.db))?; + } + Argument::Variadic => { + write!(f, "*{}", ty.unwrap_or_else(Type::unknown).display(self.db))?; + } + Argument::Keyword(name) => write!( + f, + "{}={}", + name, + ty.unwrap_or_else(Type::unknown).display(self.db) + )?, + Argument::Keywords => { + write!(f, "**{}", ty.unwrap_or_else(Type::unknown).display(self.db))?; + } + } + } + f.write_str(")") + } + } + + DisplayCallArguments { + call_arguments: self, + db, + } + } } /// Represents a single element of the expansion process for argument types for [`expand`]. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 29b176ec8a..e81d26d8b8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1603,10 +1603,16 @@ impl<'db> CallableBinding<'db> { // before checking. let argument_types = argument_types.with_self(self.bound_type); - tracing::debug!( + let _span = tracing::trace_span!( + "CallableBinding::check_types", + arguments = %argument_types.display(db), + signature = %self.signature_type.display(db), + ) + .entered(); + + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 1", ); @@ -1640,10 +1646,9 @@ impl<'db> CallableBinding<'db> { overload.check_types(db, argument_types.as_ref(), call_expression_tcx); } - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 2", ); @@ -1659,10 +1664,9 @@ impl<'db> CallableBinding<'db> { // If two or more candidate overloads remain, proceed to step 4. self.filter_overloads_containing_variadic(&indexes); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 4", ); @@ -1685,10 +1689,9 @@ impl<'db> CallableBinding<'db> { &indexes, ); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 5", ); } @@ -1793,10 +1796,9 @@ impl<'db> CallableBinding<'db> { overload.match_parameters(db, expanded_arguments, &mut argument_forms); } - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 1", ); @@ -1806,10 +1808,9 @@ impl<'db> CallableBinding<'db> { overload.check_types(db, expanded_arguments, call_expression_tcx); } - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 2", ); @@ -1821,10 +1822,9 @@ impl<'db> CallableBinding<'db> { MatchingOverloadIndex::Multiple(matching_overload_indexes) => { self.filter_overloads_containing_variadic(&matching_overload_indexes); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 4", ); @@ -1843,10 +1843,9 @@ impl<'db> CallableBinding<'db> { &indexes, ); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 5", ); From 5c320990f7c3664370636deeb0e31b9c5cd04e50 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Dec 2025 03:40:19 -0500 Subject: [PATCH 18/36] [ty] Avoid inferring types for invalid binary expressions in string annotations (#21911) ## Summary Closes https://github.com/astral-sh/ty/issues/1847. --------- Co-authored-by: David Peter --- .../ty_python_semantic/resources/mdtest/annotations/string.md | 3 +++ .../src/types/infer/builder/type_expression.rs | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 5777070441..db152ae907 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -156,6 +156,9 @@ a: "1 or 2" b: "(x := 1)" # error: [invalid-type-form] c: "1 + 2" +# Regression test for https://github.com/astral-sh/ty/issues/1847 +# error: [invalid-type-form] +c2: "a*(i for i in [])" d: "lambda x: x" e: "x if True else y" f: "{'a': 1, 'b': 2}" 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 fbae2c8948..a3ffabe826 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 @@ -155,7 +155,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // anything else is an invalid annotation: op => { - self.infer_binary_expression(binary, TypeContext::default()); self.report_invalid_type_expression( expression, format_args!( From aa27925e8707bb414a60ae73c85d3cb6cc0d02fc Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 11 Dec 2025 11:45:18 +0100 Subject: [PATCH 19/36] Skip over trivia tokens after re-lexing (#21895) --- Cargo.lock | 1 + crates/ruff_python_parser/Cargo.toml | 1 + .../resources/invalid/re_lexing/ty_1828.py | 5 + crates/ruff_python_parser/src/token_source.rs | 57 ++++- crates/ruff_python_parser/tests/fixtures.rs | 65 ++++- ...nvalid_syntax@re_lex_logical_token.py.snap | 24 +- ...yntax@re_lex_logical_token_mac_eol.py.snap | 12 +- ...x@re_lex_logical_token_windows_eol.py.snap | 14 +- .../invalid_syntax@re_lexing__ty_1828.py.snap | 227 ++++++++++++++++++ 9 files changed, 373 insertions(+), 33 deletions(-) create mode 100644 crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap diff --git a/Cargo.lock b/Cargo.lock index d0018e76e3..b6ca38375b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3349,6 +3349,7 @@ dependencies = [ "compact_str", "get-size2", "insta", + "itertools 0.14.0", "memchr", "ruff_annotate_snippets", "ruff_python_ast", diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index ae45871866..c527f96e11 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -35,6 +35,7 @@ ruff_source_file = { workspace = true } anyhow = { workspace = true } insta = { workspace = true, features = ["glob"] } +itertools = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } walkdir = { workspace = true } diff --git a/crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py b/crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py new file mode 100644 index 0000000000..3e953b541f --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py @@ -0,0 +1,5 @@ +# Regression test for https://github.com/astral-sh/ty/issues/1828 +(c: int = 1,f"""{d=[ +def a( +class A: + pass diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index e5755806e3..e22196e00e 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -67,26 +67,59 @@ impl<'src> TokenSource<'src> { /// /// [`re_lex_logical_token`]: Lexer::re_lex_logical_token pub(crate) fn re_lex_logical_token(&mut self) { - let mut non_logical_newline_start = None; - for token in self.tokens.iter().rev() { + let mut non_logical_newline = None; + + #[cfg(debug_assertions)] + let last_non_trivia_end_before = { + self.tokens + .iter() + .rev() + .find(|tok| !tok.kind().is_trivia()) + .map(ruff_text_size::Ranged::end) + }; + + for (index, token) in self.tokens.iter().enumerate().rev() { match token.kind() { TokenKind::NonLogicalNewline => { - non_logical_newline_start = Some(token.start()); + non_logical_newline = Some((index, token.start())); } TokenKind::Comment => continue, _ => break, } } - if self.lexer.re_lex_logical_token(non_logical_newline_start) { - let current_start = self.current_range().start(); - while self - .tokens - .last() - .is_some_and(|last| last.start() >= current_start) - { - self.tokens.pop(); - } + if !self + .lexer + .re_lex_logical_token(non_logical_newline.map(|(_, start)| start)) + { + return; + } + + let non_logical_line_index = non_logical_newline + .expect( + "`re_lex_logical_token` should only return `true` if `non_logical_line` is `Some`", + ) + .0; + + // Trim the already bumped logical line token (and comments coming after it) as it might now have become a logical line token + self.tokens.truncate(non_logical_line_index); + + #[cfg(debug_assertions)] + { + let last_non_trivia_end_now = { + self.tokens + .iter() + .rev() + .find(|tok| !tok.kind().is_trivia()) + .map(ruff_text_size::Ranged::end) + }; + + assert_eq!(last_non_trivia_end_before, last_non_trivia_end_now); + } + + // Ensure `current` is positioned at a non-trivia token. + if self.current_kind().is_trivia() { + self.bump(self.current_kind()); } } diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 2c89ba7aad..a9378eddfe 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -4,15 +4,16 @@ use std::fmt::{Formatter, Write}; use std::fs; use std::path::Path; +use itertools::Itertools; use ruff_annotate_snippets::{Level, Renderer, Snippet}; -use ruff_python_ast::token::Token; +use ruff_python_ast::token::{Token, Tokens}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; use ruff_python_ast::{self as ast, AnyNodeRef, Mod, PythonVersion}; use ruff_python_parser::semantic_errors::{ SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, }; -use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, parse_unchecked}; +use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, Parsed, parse_unchecked}; use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -81,7 +82,7 @@ fn test_valid_syntax(input_path: &Path) { } validate_tokens(parsed.tokens(), source.text_len(), input_path); - validate_ast(parsed.syntax(), source.text_len(), input_path); + validate_ast(&parsed, source.text_len(), input_path); let mut output = String::new(); writeln!(&mut output, "## AST").unwrap(); @@ -139,7 +140,7 @@ fn test_invalid_syntax(input_path: &Path) { let parsed = parse_unchecked(&source, options.clone()); validate_tokens(parsed.tokens(), source.text_len(), input_path); - validate_ast(parsed.syntax(), source.text_len(), input_path); + validate_ast(&parsed, source.text_len(), input_path); let mut output = String::new(); writeln!(&mut output, "## AST").unwrap(); @@ -402,12 +403,16 @@ Tokens: {tokens:#?} /// * the range of the parent node fully encloses all its child nodes /// * the ranges are strictly increasing when traversing the nodes in pre-order. /// * all ranges are within the length of the source code. -fn validate_ast(root: &Mod, source_len: TextSize, test_path: &Path) { - walk_module(&mut ValidateAstVisitor::new(source_len, test_path), root); +fn validate_ast(parsed: &Parsed, source_len: TextSize, test_path: &Path) { + walk_module( + &mut ValidateAstVisitor::new(parsed.tokens(), source_len, test_path), + parsed.syntax(), + ); } #[derive(Debug)] struct ValidateAstVisitor<'a> { + tokens: std::iter::Peekable>, parents: Vec>, previous: Option>, source_length: TextSize, @@ -415,8 +420,9 @@ struct ValidateAstVisitor<'a> { } impl<'a> ValidateAstVisitor<'a> { - fn new(source_length: TextSize, test_path: &'a Path) -> Self { + fn new(tokens: &'a Tokens, source_length: TextSize, test_path: &'a Path) -> Self { Self { + tokens: tokens.iter().peekable(), parents: Vec::new(), previous: None, source_length, @@ -425,6 +431,47 @@ impl<'a> ValidateAstVisitor<'a> { } } +impl ValidateAstVisitor<'_> { + /// Check that the node's start doesn't fall within a token. + /// Called in `enter_node` before visiting children. + fn assert_start_boundary(&mut self, node: AnyNodeRef<'_>) { + // Skip tokens that end at or before the node starts. + self.tokens + .peeking_take_while(|t| t.end() <= node.start()) + .last(); + + if let Some(next) = self.tokens.peek() { + // At this point, next_token.end() > node.start() + assert!( + next.start() >= node.start(), + "{path}: The start of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + path = self.test_path.display(), + root = self.parents.first() + ); + } + } + + /// Check that the node's end doesn't fall within a token. + /// Called in `leave_node` after visiting children, so all tokens + /// within the node have been consumed. + fn assert_end_boundary(&mut self, node: AnyNodeRef<'_>) { + // Skip tokens that end at or before the node ends. + self.tokens + .peeking_take_while(|t| t.end() <= node.end()) + .last(); + + if let Some(next) = self.tokens.peek() { + // At this point, `next_token.end() > node.end()` + assert!( + next.start() >= node.end(), + "{path}: The end of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + path = self.test_path.display(), + root = self.parents.first() + ); + } + } +} + impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal { assert!( @@ -452,12 +499,16 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { ); } + self.assert_start_boundary(node); + self.parents.push(node); TraversalSignal::Traverse } fn leave_node(&mut self, node: AnyNodeRef<'ast>) { + self.assert_end_boundary(node); + self.parents.pop().expect("Expected tree to be balanced"); self.previous = Some(node); 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 61f3230855..93b1439a58 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 @@ -296,7 +296,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 456..472, + range: 456..471, func: Name( ExprName { node_index: NodeIndex(None), @@ -306,7 +306,7 @@ Module( }, ), arguments: Arguments { - range: 460..472, + range: 460..471, node_index: NodeIndex(None), args: [ Name( @@ -581,7 +581,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 890..906, + range: 890..905, func: Name( ExprName { node_index: NodeIndex(None), @@ -591,7 +591,7 @@ Module( }, ), arguments: Arguments { - range: 894..906, + range: 894..905, node_index: NodeIndex(None), args: [ FString( @@ -832,7 +832,16 @@ Module( | 28 | # The lexer is nested with multiple levels of parentheses 29 | if call(foo, [a, b - | ^ Syntax Error: Expected `]`, found NonLogicalNewline +30 | def bar(): + | ^^^ Syntax Error: Expected `]`, found `def` +31 | pass + | + + + | +28 | # The lexer is nested with multiple levels of parentheses +29 | if call(foo, [a, b + | ^ Syntax Error: Expected `)`, found newline 30 | def bar(): 31 | pass | @@ -857,11 +866,10 @@ Module( | -41 | # test is to make sure it emits a `NonLogicalNewline` token after `b`. 42 | if call(foo, [a, 43 | b - | ^ Syntax Error: Expected `]`, found NonLogicalNewline 44 | ) + | ^ Syntax Error: Expected `]`, found `)` 45 | def bar(): 46 | pass | @@ -898,7 +906,7 @@ Module( | 49 | # F-strings uses normal list parsing, so test those as well 50 | if call(f"hello {x - | ^ Syntax Error: Expected FStringEnd, found NonLogicalNewline + | ^ Syntax Error: Expected `)`, found newline 51 | def bar(): 52 | pass | 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 5567459c70..2eb9515848 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 @@ -17,7 +17,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 3..19, + range: 3..18, func: Name( ExprName { node_index: NodeIndex(None), @@ -27,7 +27,7 @@ Module( }, ), arguments: Arguments { - range: 7..19, + range: 7..18, node_index: NodeIndex(None), args: [ Name( @@ -113,5 +113,11 @@ Module( | 1 | if call(foo, [a, b def bar(): pass - | ^ Syntax Error: Expected `]`, found NonLogicalNewline + | ^^^ Syntax Error: Expected `]`, found `def` + | + + + | +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_lex_logical_token_windows_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap index ae03fee095..692755d4e8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap @@ -17,7 +17,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 3..20, + range: 3..18, func: Name( ExprName { node_index: NodeIndex(None), @@ -27,7 +27,7 @@ Module( }, ), arguments: Arguments { - range: 7..20, + range: 7..18, node_index: NodeIndex(None), args: [ Name( @@ -113,7 +113,15 @@ Module( | 1 | if call(foo, [a, b - | ^ Syntax Error: Expected `]`, found NonLogicalNewline +2 | def bar(): + | ^^^ Syntax Error: Expected `]`, found `def` +3 | pass + | + + + | +1 | if call(foo, [a, b + | ^ Syntax Error: Expected `)`, found newline 2 | def bar(): 3 | pass | 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 new file mode 100644 index 0000000000..49ea0c7d58 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap @@ -0,0 +1,227 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..112, + body: [ + AnnAssign( + StmtAnnAssign { + node_index: NodeIndex(None), + range: 66..93, + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 67..68, + id: Name("c"), + ctx: Store, + }, + ), + annotation: Name( + ExprName { + node_index: NodeIndex(None), + range: 70..73, + id: Name("int"), + ctx: Load, + }, + ), + value: Some( + Tuple( + ExprTuple { + node_index: NodeIndex(None), + range: 76..93, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 76..77, + value: Int( + 1, + ), + }, + ), + Subscript( + ExprSubscript { + node_index: NodeIndex(None), + range: 78..90, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 78..85, + value: FStringValue { + inner: Single( + FString( + FString { + range: 78..85, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 82..85, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 83..84, + id: Name("d"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + unclosed: true, + }, + }, + ), + ), + }, + }, + ), + slice: Slice( + ExprSlice { + node_index: NodeIndex(None), + range: 87..90, + lower: None, + upper: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 87..90, + id: Name("def"), + ctx: Load, + }, + ), + ), + step: None, + }, + ), + ctx: Load, + }, + ), + Call( + ExprCall { + node_index: NodeIndex(None), + range: 91..93, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 91..92, + id: Name("a"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 92..93, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + simple: false, + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ + | ^ Syntax Error: Expected `)`, found `:` +3 | def a( +4 | class A: + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ + | ^ Syntax Error: f-string: expecting `}` +3 | def a( +4 | class A: + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | ^^^ Syntax Error: Expected `:`, found `def` +4 | class A: +5 | pass + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | ^ Syntax Error: Expected `]`, found name +4 | class A: +5 | pass + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | _______^ +4 | | class A: +5 | | pass + | |_________^ Syntax Error: f-string: unterminated triple-quoted string + | + + + | +2 | (c: int = 1,f"""{d=[ +3 | def a( +4 | class A: + | ^^^^^ Syntax Error: Expected `)`, found `class` +5 | pass + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | _______^ +4 | | class A: +5 | | pass + | |_________^ Syntax Error: Expected a statement + | + + + | +4 | class A: +5 | pass + | ^ Syntax Error: unexpected EOF while parsing + | From 71540c03b6fdf0c56a14b5f0d95b9347c86eaa9a Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 11 Dec 2025 12:57:45 +0100 Subject: [PATCH 20/36] [ty] Revert "Do not infer types for invalid binary expressions in annotations" (#21914) See discussion here: https://github.com/astral-sh/ruff/pull/21911#discussion_r2610155157 --- .../src/types/infer/builder/type_expression.rs | 6 ++++++ 1 file changed, 6 insertions(+) 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 a3ffabe826..626225cdc8 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 @@ -155,6 +155,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // anything else is an invalid annotation: op => { + // Avoid inferring the types of invalid binary expressions that have been + // parsed from a string annotation, as they are not present in the semantic + // index. + if !self.deferred_state.in_string_annotation() { + self.infer_binary_expression(binary, TypeContext::default()); + } self.report_invalid_type_expression( expression, format_args!( From 27912d46b1af2db0d290b3598e46aca04a35127f Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Thu, 11 Dec 2025 13:04:57 +0100 Subject: [PATCH 21/36] Remove `BackwardsTokenizer` based `parenthesized_range` references in `ruff_linter` (#21836) Co-authored-by: Micha Reiser --- crates/ruff_linter/src/checkers/ast/mod.rs | 9 ++++ crates/ruff_linter/src/fix/edits.rs | 28 ++++------- .../rules/fastapi_redundant_response_model.rs | 4 +- .../rules/map_without_explicit_strict.rs | 7 +-- .../rules/mutable_argument_default.rs | 18 ++----- .../rules/no_explicit_stacklevel.rs | 7 +-- .../rules/zip_without_explicit_strict.rs | 7 +-- .../rules/unnecessary_generator_list.rs | 12 ++--- .../rules/unnecessary_generator_set.rs | 12 ++--- .../unnecessary_list_comprehension_set.rs | 12 ++--- .../rules/explicit.rs | 10 +--- .../rules/exc_info_outside_except_handler.rs | 5 +- .../rules/unnecessary_dict_kwargs.rs | 9 ++-- .../rules/unnecessary_range_start.rs | 6 +-- .../rules/generic_not_last_base_class.rs | 10 ++-- .../rules/redundant_none_literal.rs | 30 ++++-------- .../flake8_pytest_style/rules/assertion.rs | 5 +- .../flake8_pytest_style/rules/fixture.rs | 4 +- .../flake8_pytest_style/rules/parametrize.rs | 35 +++----------- .../flake8_simplify/rules/ast_bool_op.rs | 24 +++------- .../rules/flake8_simplify/rules/ast_ifexp.rs | 11 ++--- .../rules/if_with_same_arms.rs | 48 ++++++++----------- .../flake8_simplify/rules/key_in_dict.rs | 20 ++------ .../rules/type_alias_quotes.rs | 13 +++-- .../path_constructor_current_directory.rs | 16 ++----- .../pandas_vet/rules/inplace_argument.rs | 20 +++----- .../pycodestyle/rules/lambda_assignment.rs | 30 ++++-------- .../pycodestyle/rules/literal_comparisons.rs | 24 ++++------ .../src/rules/pycodestyle/rules/not_tests.rs | 4 +- .../src/rules/pyflakes/rules/repeated_keys.rs | 14 ++---- .../rules/pyflakes/rules/unused_variable.rs | 14 ++---- .../rules/boolean_chained_comparison.rs | 42 +++++----------- .../src/rules/pylint/rules/duplicate_bases.rs | 2 +- .../src/rules/pylint/rules/if_stmt_min_max.rs | 11 ++--- .../pylint/rules/missing_maxsplit_arg.rs | 8 +--- .../pylint/rules/non_augmented_assignment.rs | 8 ++-- .../rules/subprocess_run_without_check.rs | 7 +-- .../pylint/rules/unspecified_encoding.rs | 6 +-- .../rules/pep695/non_pep695_generic_class.rs | 2 +- .../rules/pep695/non_pep695_type_alias.rs | 6 +-- .../pyupgrade/rules/replace_stdout_stderr.rs | 9 ++-- .../rules/replace_universal_newlines.rs | 2 +- .../rules/unnecessary_encode_utf8.rs | 8 ++-- .../rules/useless_class_metaclass_type.rs | 2 +- .../rules/useless_object_inheritance.rs | 2 +- .../pyupgrade/rules/yield_in_for_loop.rs | 11 ++--- .../ruff_linter/src/rules/refurb/helpers.rs | 10 ++-- .../refurb/rules/fromisoformat_replace_z.rs | 5 +- .../rules/if_exp_instead_of_or_operator.rs | 16 ++----- .../rules/refurb/rules/readlines_in_for.rs | 5 +- .../rules/refurb/rules/redundant_log_base.rs | 11 ++--- .../rules/single_item_membership_test.rs | 2 +- .../ruff/rules/class_with_mixed_type_vars.rs | 2 +- .../rules/ruff/rules/default_factory_kwarg.rs | 11 ++--- .../ruff/rules/falsy_dict_get_fallback.rs | 2 +- .../rules/parenthesize_chained_operators.rs | 11 +---- .../src/rules/ruff/rules/post_init_default.rs | 5 +- .../ruff/rules/quadratic_list_summation.rs | 11 ++--- .../src/rules/ruff/rules/starmap_zip.rs | 20 ++------ .../ruff/rules/unnecessary_cast_to_int.rs | 33 ++++++------- .../rules/ruff/rules/unnecessary_key_check.rs | 20 ++------ .../unnecessary_literal_within_deque_call.rs | 22 ++------- crates/ruff_python_ast/src/helpers.rs | 12 ++--- 63 files changed, 263 insertions(+), 529 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 4d4d7e9293..37422cbf18 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -437,6 +437,15 @@ impl<'a> Checker<'a> { } } + /// Returns the [`Tokens`] for the parsed source file. + /// + /// + /// Unlike [`Self::tokens`], this method always returns + /// the tokens for the current file, even when within a parsed type annotation. + pub(crate) fn source_tokens(&self) -> &'a Tokens { + self.parsed.tokens() + } + /// The [`Locator`] for the current file, which enables extraction of source code from byte /// offsets. pub(crate) const fn locator(&self) -> &'a Locator<'a> { diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs index 05e90519eb..20f50d6e10 100644 --- a/crates/ruff_linter/src/fix/edits.rs +++ b/crates/ruff_linter/src/fix/edits.rs @@ -3,14 +3,13 @@ use anyhow::{Context, Result}; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{self, Tokens, parenthesized_range}; use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_trivia::textwrap::dedent_to; use ruff_python_trivia::{ - CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, - is_python_whitespace, + PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace, }; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -209,7 +208,7 @@ pub(crate) fn remove_argument( arguments: &Arguments, parentheses: Parentheses, source: &str, - comment_ranges: &CommentRanges, + tokens: &Tokens, ) -> Result { // Partition into arguments before and after the argument to remove. let (before, after): (Vec<_>, Vec<_>) = arguments @@ -224,7 +223,7 @@ pub(crate) fn remove_argument( .context("Unable to find argument")?; let parenthesized_range = - parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source) + token::parenthesized_range(arg.value().into(), arguments.into(), tokens) .unwrap_or(arg.range()); if !after.is_empty() { @@ -270,25 +269,14 @@ pub(crate) fn remove_argument( /// /// The new argument will be inserted before the first existing keyword argument in `arguments`, if /// there are any present. Otherwise, the new argument is added to the end of the argument list. -pub(crate) fn add_argument( - argument: &str, - arguments: &Arguments, - comment_ranges: &CommentRanges, - source: &str, -) -> Edit { +pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Tokens) -> Edit { if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() { - let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source) - .unwrap_or(*range); + let keyword = parenthesized_range(value.into(), arguments.into(), tokens).unwrap_or(*range); Edit::insertion(format!("{argument}, "), keyword.start()) } else if let Some(last) = arguments.arguments_source_order().last() { // Case 1: existing arguments, so append after the last argument. - let last = parenthesized_range( - last.value().into(), - arguments.into(), - comment_ranges, - source, - ) - .unwrap_or(last.range()); + let last = parenthesized_range(last.value().into(), arguments.into(), tokens) + .unwrap_or(last.range()); Edit::insertion(format!(", {argument}"), last.end()) } else { // Case 2: no arguments. Add argument, without any trailing comma. diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs index 440be901a7..d31ffbf61e 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs @@ -91,8 +91,8 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def: response_model_arg, &call.arguments, Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::unsafe_edit) }); 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 cd268f610b..04e9640e09 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 @@ -74,12 +74,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal checker .report_diagnostic(MapWithoutExplicitStrict, call.range()) .set_fix(Fix::applicable_edit( - add_argument( - "strict=False", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ), + add_argument("strict=False", &call.arguments, checker.tokens()), Applicability::Unsafe, )); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index 94098831b8..c0b1b5e840 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::name::QualifiedName; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, ParameterWithDefault}; use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::function_type::is_stub; @@ -166,12 +166,7 @@ fn move_initialization( return None; } - let range = match parenthesized_range( - default.into(), - parameter.into(), - checker.comment_ranges(), - checker.source(), - ) { + let range = match parenthesized_range(default.into(), parameter.into(), checker.tokens()) { Some(range) => range, None => default.range(), }; @@ -194,13 +189,8 @@ fn move_initialization( "{} = {}", parameter.parameter.name(), locator.slice( - parenthesized_range( - default.into(), - parameter.into(), - checker.comment_ranges(), - checker.source() - ) - .unwrap_or(default.range()) + parenthesized_range(default.into(), parameter.into(), checker.tokens()) + .unwrap_or(default.range()) ) ); } else { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index f737e781ef..a21cf9a30e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -92,12 +92,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) { } let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range()); - let edit = add_argument( - "stacklevel=2", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ); + let edit = add_argument("stacklevel=2", &call.arguments, checker.tokens()); diagnostic.set_fix(Fix::unsafe_edit(edit)); } 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 136715b981..db71c7b2fb 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 @@ -70,12 +70,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal checker .report_diagnostic(ZipWithoutExplicitStrict, call.range()) .set_fix(Fix::applicable_edit( - add_argument( - "strict=False", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ), + add_argument("strict=False", &call.arguments, checker.tokens()), Applicability::Unsafe, )); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index 5fdc1a37a3..d271a13792 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::ExprGenerator; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::TokenKind; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -142,13 +142,9 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall if *parenthesized { // The generator's range will include the innermost parentheses, but it could be // surrounded by additional parentheses. - let range = parenthesized_range( - argument.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(argument.range()); + let range = + parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or(argument.range()); // The generator always parenthesizes the expression; trim the parentheses. let generator = checker.generator().expr(argument); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 0560935bae..05a1c523cf 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::ExprGenerator; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::TokenKind; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -147,13 +147,9 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall) if *parenthesized { // The generator's range will include the innermost parentheses, but it could be // surrounded by additional parentheses. - let range = parenthesized_range( - argument.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(argument.range()); + let range = + parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or(argument.range()); // The generator always parenthesizes the expression; trim the parentheses. let generator = checker.generator().expr(argument); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index b4fda738e2..f6699500af 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -1,7 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::TokenKind; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -89,13 +89,9 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast:: // If the list comprehension is parenthesized, remove the parentheses in addition to // removing the brackets. - let replacement_range = parenthesized_range( - argument.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or_else(|| argument.range()); + let replacement_range = + parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or_else(|| argument.range()); let span = argument.range().add_start(one).sub_end(one); let replacement = diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs index 8edb7b46ad..e742b8922d 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_trivia::is_python_whitespace; use ruff_source_file::LineRanges; @@ -88,13 +88,7 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) { checker.report_diagnostic(ExplicitStringConcatenation, expr.range()); let is_parenthesized = |expr: &Expr| { - parenthesized_range( - expr.into(), - bin_op.into(), - checker.comment_ranges(), - checker.source(), - ) - .is_some() + parenthesized_range(expr.into(), bin_op.into(), checker.tokens()).is_some() }; // If either `left` or `right` is parenthesized, generating // a fix would be too involved. Just report the diagnostic. diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs index 1172e40893..7d18897708 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs @@ -111,7 +111,6 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall } let arguments = &call.arguments; - let source = checker.source(); let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range); @@ -120,8 +119,8 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall exc_info, arguments, Parentheses::Preserve, - source, - checker.comment_ranges(), + checker.source(), + checker.tokens(), )?; Ok(Fix::unsafe_edit(edit)) }); diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs index cc436d258a..1b3faba504 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::Ranged; @@ -129,8 +129,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { keyword, &call.arguments, Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -158,8 +158,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { parenthesized_range( value.into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens() ) .unwrap_or(value.range()) ) diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs index 6e6b10b206..e12af6e069 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs @@ -73,11 +73,11 @@ pub(crate) fn unnecessary_range_start(checker: &Checker, call: &ast::ExprCall) { let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range()); diagnostic.try_set_fix(|| { remove_argument( - &start, + start, &call.arguments, Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::safe_edit) }); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs index 4cd5035693..90b8476809 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs @@ -160,20 +160,16 @@ fn generate_fix( ) -> anyhow::Result { let locator = checker.locator(); let source = locator.contents(); + let tokens = checker.tokens(); let deletion = remove_argument( generic_base, arguments, Parentheses::Preserve, source, - checker.comment_ranges(), + tokens, )?; - let insertion = add_argument( - locator.slice(generic_base), - arguments, - checker.comment_ranges(), - source, - ); + let insertion = add_argument(locator.slice(generic_base), arguments, tokens); Ok(Fix::unsafe_edits(deletion, [insertion])) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 14145229fc..b3e35c21c2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -5,7 +5,7 @@ use ruff_python_ast::{ helpers::{pep_604_union, typing_optional}, name::Name, operator_precedence::OperatorPrecedence, - parenthesize::parenthesized_range, + token::{Tokens, parenthesized_range}, }; use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union}; use ruff_text_size::{Ranged, TextRange}; @@ -243,16 +243,12 @@ fn create_fix( let union_expr = pep_604_union(&[new_literal_expr, none_expr]); // Check if we need parentheses to preserve operator precedence - let content = if needs_parentheses_for_precedence( - semantic, - literal_expr, - checker.comment_ranges(), - checker.source(), - ) { - format!("({})", checker.generator().expr(&union_expr)) - } else { - checker.generator().expr(&union_expr) - }; + let content = + if needs_parentheses_for_precedence(semantic, literal_expr, checker.tokens()) { + format!("({})", checker.generator().expr(&union_expr)) + } else { + checker.generator().expr(&union_expr) + }; let union_edit = Edit::range_replacement(content, literal_expr.range()); Fix::applicable_edit(union_edit, applicability) @@ -278,8 +274,7 @@ enum UnionKind { fn needs_parentheses_for_precedence( semantic: &ruff_python_semantic::SemanticModel, literal_expr: &Expr, - comment_ranges: &ruff_python_trivia::CommentRanges, - source: &str, + tokens: &Tokens, ) -> bool { // Get the parent expression to check if we're in a context that needs parentheses let Some(parent_expr) = semantic.current_expression_parent() else { @@ -287,14 +282,7 @@ fn needs_parentheses_for_precedence( }; // Check if the literal expression is already parenthesized - if parenthesized_range( - literal_expr.into(), - parent_expr.into(), - comment_ranges, - source, - ) - .is_some() - { + if parenthesized_range(literal_expr.into(), parent_expr.into(), tokens).is_some() { return false; // Already parenthesized, don't add more } 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 545372dd6c..c97efd8b05 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 @@ -10,7 +10,7 @@ use libcst_native::{ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::Truthiness; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{ self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp, @@ -303,8 +303,7 @@ pub(crate) fn unittest_assertion( parenthesized_range( expr.into(), checker.semantic().current_statement().into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or(expr.range()), ))); 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 49be564b48..c939d44346 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 @@ -768,8 +768,8 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor keyword, arguments, edits::Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::unsafe_edit) }); diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs index 20b14399f9..904ca1c494 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -2,10 +2,9 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags}; use ruff_python_codegen::Generator; -use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -322,18 +321,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) - /// ``` /// /// This method assumes that the first argument is a string. -fn get_parametrize_name_range( - call: &ExprCall, - expr: &Expr, - comment_ranges: &CommentRanges, - source: &str, -) -> Option { - parenthesized_range( - expr.into(), - (&call.arguments).into(), - comment_ranges, - source, - ) +fn get_parametrize_name_range(call: &ExprCall, expr: &Expr, tokens: &Tokens) -> Option { + parenthesized_range(expr.into(), (&call.arguments).into(), tokens) } /// PT006 @@ -349,13 +338,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr if names.len() > 1 { match names_type { types::ParametrizeNameType::Tuple => { - let name_range = get_parametrize_name_range( - call, - expr, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(expr.range()); + let name_range = get_parametrize_name_range(call, expr, checker.tokens()) + .unwrap_or(expr.range()); let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, @@ -386,13 +370,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr ))); } types::ParametrizeNameType::List => { - let name_range = get_parametrize_name_range( - call, - expr, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(expr.range()); + let name_range = get_parametrize_name_range(call, expr, checker.tokens()) + .unwrap_or(expr.range()); let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, 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 0b53a271f4..5de313a600 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 @@ -10,7 +10,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::{Truthiness, contains_effect}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; @@ -800,14 +800,9 @@ fn is_short_circuit( edit = Some(get_short_circuit_edit( value, TextRange::new( - parenthesized_range( - furthest.into(), - expr.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(furthest.range()) - .start(), + parenthesized_range(furthest.into(), expr.into(), checker.tokens()) + .unwrap_or(furthest.range()) + .start(), expr.end(), ), short_circuit_truthiness, @@ -828,14 +823,9 @@ fn is_short_circuit( edit = Some(get_short_circuit_edit( next_value, TextRange::new( - parenthesized_range( - furthest.into(), - expr.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(furthest.range()) - .start(), + parenthesized_range(furthest.into(), expr.into(), checker.tokens()) + .unwrap_or(furthest.range()) + .start(), expr.end(), ), short_circuit_truthiness, 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 15b2f54bf8..2292f4e581 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 @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::{is_const_false, is_const_true}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use crate::checkers::ast::Checker; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; @@ -171,13 +171,8 @@ pub(crate) fn if_expr_with_true_false( checker .locator() .slice( - parenthesized_range( - test.into(), - expr.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(test.range()), + parenthesized_range(test.into(), expr.into(), checker.tokens()) + .unwrap_or(test.range()), ) .to_string(), expr.range(), diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs index 92182b383a..17b7c5c612 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs @@ -4,10 +4,10 @@ use anyhow::Result; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableStmt; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches}; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; @@ -99,7 +99,7 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) { ¤t_branch, following_branch, checker.locator(), - checker.comment_ranges(), + checker.tokens(), ) }); } @@ -111,7 +111,7 @@ fn merge_branches( current_branch: &IfElifBranch, following_branch: &IfElifBranch, locator: &Locator, - comment_ranges: &CommentRanges, + tokens: &ruff_python_ast::token::Tokens, ) -> Result { // Identify the colon (`:`) at the end of the current branch's test. let Some(current_branch_colon) = @@ -127,12 +127,9 @@ fn merge_branches( ); // If the following test isn't parenthesized, consider parenthesizing it. - let following_branch_test = if let Some(range) = parenthesized_range( - following_branch.test.into(), - stmt_if.into(), - comment_ranges, - locator.contents(), - ) { + let following_branch_test = if let Some(range) = + parenthesized_range(following_branch.test.into(), stmt_if.into(), tokens) + { Cow::Borrowed(locator.slice(range)) } else if matches!( following_branch.test, @@ -153,24 +150,19 @@ fn merge_branches( // // For example, if the current test is `x if x else y`, we should parenthesize it to // `(x if x else y) or ...`. - let parenthesize_edit = if matches!( - current_branch.test, - Expr::Lambda(_) | Expr::Named(_) | Expr::If(_) - ) && parenthesized_range( - current_branch.test.into(), - stmt_if.into(), - comment_ranges, - locator.contents(), - ) - .is_none() - { - Some(Edit::range_replacement( - format!("({})", locator.slice(current_branch.test)), - current_branch.test.range(), - )) - } else { - None - }; + let parenthesize_edit = + if matches!( + current_branch.test, + Expr::Lambda(_) | Expr::Named(_) | Expr::If(_) + ) && parenthesized_range(current_branch.test.into(), stmt_if.into(), tokens).is_none() + { + Some(Edit::range_replacement( + format!("({})", locator.slice(current_branch.test)), + current_branch.test.range(), + )) + } else { + None + }; Ok(Fix::safe_edits( deletion_edit, 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 5ced08a673..645a79d9e1 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 @@ -1,6 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr}; use ruff_python_semantic::analyze::typing; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; @@ -90,20 +90,10 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa } // Extract the exact range of the left and right expressions. - let left_range = parenthesized_range( - left.into(), - parent, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(left.range()); - let right_range = parenthesized_range( - right.into(), - parent, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(right.range()); + let left_range = + parenthesized_range(left.into(), parent, checker.tokens()).unwrap_or(left.range()); + let right_range = + parenthesized_range(right.into(), parent, checker.tokens()).unwrap_or(right.range()); let mut diagnostic = checker.report_diagnostic( InDictKeys { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 776ce1486e..c07de4b813 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -11,7 +11,7 @@ use crate::registry::Rule; use crate::rules::flake8_type_checking::helpers::quote_type_expression; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; /// ## What it does /// Checks if [PEP 613] explicit type aliases contain references to @@ -295,21 +295,20 @@ pub(crate) fn quoted_type_alias( let range = annotation_expr.range(); let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range); let fix_string = annotation_expr.value.to_string(); + let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r')) && parenthesized_range( - // Check for parenthesis outside string ("""...""") + // Check for parentheses outside the string ("""...""") annotation_expr.into(), checker.semantic().current_statement().into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.source_tokens(), ) .is_none() && parenthesized_range( - // Check for parenthesis inside string """(...)""" + // Check for parentheses inside the string """(...)""" expr.into(), annotation_expr.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .is_none() { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs index bf5a4ed8b7..840befca76 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -1,10 +1,9 @@ use std::ops::Range; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator}; use ruff_python_semantic::SemanticModel; -use ruff_python_trivia::CommentRanges; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -89,11 +88,7 @@ pub(crate) fn path_constructor_current_directory( let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range()); - match parent_and_next_path_fragment_range( - checker.semantic(), - checker.comment_ranges(), - checker.source(), - ) { + match parent_and_next_path_fragment_range(checker.semantic(), checker.tokens()) { Some((parent_range, next_fragment_range)) => { let next_fragment_expr = checker.locator().slice(next_fragment_range); let call_expr = checker.locator().slice(call.range()); @@ -116,7 +111,7 @@ pub(crate) fn path_constructor_current_directory( arguments, Parentheses::Preserve, checker.source(), - checker.comment_ranges(), + checker.tokens(), )?; Ok(Fix::applicable_edit(edit, applicability(call.range()))) }), @@ -125,8 +120,7 @@ pub(crate) fn path_constructor_current_directory( fn parent_and_next_path_fragment_range( semantic: &SemanticModel, - comment_ranges: &CommentRanges, - source: &str, + tokens: &ruff_python_ast::token::Tokens, ) -> Option<(TextRange, TextRange)> { let parent = semantic.current_expression_parent()?; @@ -142,6 +136,6 @@ fn parent_and_next_path_fragment_range( Some(( parent.range(), - parenthesized_range(right.into(), parent.into(), comment_ranges, source).unwrap_or(range), + parenthesized_range(right.into(), parent.into(), tokens).unwrap_or(range), )) } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs index 0fa448f7c5..71e0d82db9 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs @@ -1,8 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_python_ast::{self as ast, Keyword, Stmt}; -use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::Locator; @@ -91,7 +90,7 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) { call, keyword, statement, - checker.comment_ranges(), + checker.tokens(), checker.locator(), ) { diagnostic.set_fix(fix); @@ -111,21 +110,16 @@ fn convert_inplace_argument_to_assignment( call: &ast::ExprCall, keyword: &Keyword, statement: &Stmt, - comment_ranges: &CommentRanges, + tokens: &Tokens, locator: &Locator, ) -> Option { // Add the assignment. let attr = call.func.as_attribute_expr()?; let insert_assignment = Edit::insertion( format!("{name} = ", name = locator.slice(attr.value.range())), - parenthesized_range( - call.into(), - statement.into(), - comment_ranges, - locator.contents(), - ) - .unwrap_or(call.range()) - .start(), + parenthesized_range(call.into(), statement.into(), tokens) + .unwrap_or(call.range()) + .start(), ); // Remove the `inplace` argument. @@ -134,7 +128,7 @@ fn convert_inplace_argument_to_assignment( &call.arguments, Parentheses::Preserve, locator.contents(), - comment_ranges, + tokens, ) .ok()?; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index a42473386b..9b437aa279 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{ self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter, ParameterWithDefault, Parameters, Stmt, @@ -265,29 +265,19 @@ fn replace_trailing_ellipsis_with_original_expr( stmt: &Stmt, checker: &Checker, ) -> String { - let original_expr_range = parenthesized_range( - (&lambda.body).into(), - lambda.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(lambda.body.range()); + let original_expr_range = + parenthesized_range((&lambda.body).into(), lambda.into(), checker.tokens()) + .unwrap_or(lambda.body.range()); // This prevents the autofix of introducing a syntax error if the lambda's body is an // expression spanned across multiple lines. To avoid the syntax error we preserve // the parenthesis around the body. - let original_expr_in_source = if parenthesized_range( - lambda.into(), - stmt.into(), - checker.comment_ranges(), - checker.source(), - ) - .is_some() - { - format!("({})", checker.locator().slice(original_expr_range)) - } else { - checker.locator().slice(original_expr_range).to_string() - }; + let original_expr_in_source = + if parenthesized_range(lambda.into(), stmt.into(), checker.tokens()).is_some() { + format!("({})", checker.locator().slice(original_expr_range)) + } else { + checker.locator().slice(original_expr_range).to_string() + }; let placeholder_ellipsis_start = generated.rfind("...").unwrap(); let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len(); diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index a68e492846..5ae6fe9028 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use rustc_hash::FxHashMap; use ruff_macros::{ViolationMetadata, derive_message_formats}; @@ -179,15 +179,14 @@ fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option fn generate_redundant_comparison( compare: &ast::ExprCompare, - comment_ranges: &ruff_python_trivia::CommentRanges, + tokens: &Tokens, source: &str, comparator: &Expr, kind: bool, needs_wrap: bool, ) -> String { - let comparator_range = - parenthesized_range(comparator.into(), compare.into(), comment_ranges, source) - .unwrap_or(comparator.range()); + let comparator_range = parenthesized_range(comparator.into(), compare.into(), tokens) + .unwrap_or(comparator.range()); let comparator_str = &source[comparator_range]; @@ -379,7 +378,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) .copied() .collect::>(); - let comment_ranges = checker.comment_ranges(); + let tokens = checker.tokens(); let source = checker.source(); let content = match (&*compare.ops, &*compare.comparators) { @@ -387,18 +386,13 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) { let needs_wrap = compare.left.range().start() != compare.range().start(); generate_redundant_comparison( - compare, - comment_ranges, - source, - comparator, - kind, - needs_wrap, + compare, tokens, source, comparator, kind, needs_wrap, ) } else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) { let needs_wrap = comparator.range().end() != compare.range().end(); generate_redundant_comparison( compare, - comment_ranges, + tokens, source, &compare.left, kind, @@ -410,7 +404,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) &ops, &compare.comparators, compare.into(), - comment_ranges, + tokens, source, ) } @@ -420,7 +414,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) &ops, &compare.comparators, compare.into(), - comment_ranges, + tokens, source, ), }; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs index a4234093bc..0c759f9e0e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs @@ -107,7 +107,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { &[CmpOp::NotIn], comparators, unary_op.into(), - checker.comment_ranges(), + checker.tokens(), checker.source(), ), unary_op.range(), @@ -127,7 +127,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { &[CmpOp::IsNot], comparators, unary_op.into(), - checker.comment_ranges(), + checker.tokens(), checker.source(), ), unary_op.range(), diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index 1acdc90138..de02e4c85a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -3,7 +3,7 @@ use std::collections::hash_map::Entry; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::{ComparableExpr, HashableExpr}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -193,16 +193,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { parenthesized_range( dict.value(i - 1).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i - 1).range()) .end(), parenthesized_range( dict.value(i).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i).range()) .end(), @@ -224,16 +222,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { parenthesized_range( dict.value(i - 1).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i - 1).range()) .end(), parenthesized_range( dict.value(i).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i).range()) .end(), diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs index 810c5742b9..59dcbf5c22 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::{self as ast, Stmt}; use ruff_python_semantic::Binding; @@ -172,14 +172,10 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option { { // If the expression is complex (`x = foo()`), remove the assignment, // but preserve the right-hand side. - let start = parenthesized_range( - target.into(), - statement.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(target.range()) - .start(); + let start = + parenthesized_range(target.into(), statement.into(), checker.tokens()) + .unwrap_or(target.range()) + .start(); let end = match_token_after(checker.tokens(), target.end(), |token| { token == TokenKind::Equal })? diff --git a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs index 27d6d49ad5..9673524a93 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, - parenthesize::{parentheses_iterator, parenthesized_range}, + token::{parentheses_iterator, parenthesized_range}, }; use ruff_text_size::{Ranged, TextRange}; @@ -62,7 +62,7 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB } let locator = checker.locator(); - let comment_ranges = checker.comment_ranges(); + let tokens = checker.tokens(); // retrieve all compare expressions from boolean expression let compare_expressions = expr_bool_op @@ -89,40 +89,22 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB continue; } - let left_paren_count = parentheses_iterator( - left_compare.into(), - Some(expr_bool_op.into()), - comment_ranges, - locator.contents(), - ) - .count(); + let left_paren_count = + parentheses_iterator(left_compare.into(), Some(expr_bool_op.into()), tokens).count(); - let right_paren_count = parentheses_iterator( - right_compare.into(), - Some(expr_bool_op.into()), - comment_ranges, - locator.contents(), - ) - .count(); + let right_paren_count = + parentheses_iterator(right_compare.into(), Some(expr_bool_op.into()), tokens).count(); // Create the edit that removes the comparison operator // In `a<(b) and ((b)) "rsplit", }; - let maxsplit_argument_edit = fix::edits::add_argument( - "maxsplit=1", - arguments, - checker.comment_ranges(), - checker.locator().contents(), - ); + let maxsplit_argument_edit = + fix::edits::add_argument("maxsplit=1", arguments, checker.tokens()); // Only change `actual_split_type` if it doesn't match `suggested_split_type` let split_type_edit: Option = if actual_split_type == suggested_split_type { 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 7423c2dc76..cbe51cdd8d 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 @@ -2,7 +2,7 @@ use ast::Expr; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{ExprBinOp, ExprRef, Operator}; use ruff_text_size::{Ranged, TextRange}; @@ -150,12 +150,10 @@ fn augmented_assignment( let right_operand_ref = ExprRef::from(right_operand); let parent = original_expr.into(); - let comment_ranges = checker.comment_ranges(); - let source = checker.source(); + let tokens = checker.tokens(); let right_operand_range = - parenthesized_range(right_operand_ref, parent, comment_ranges, source) - .unwrap_or(right_operand.range()); + parenthesized_range(right_operand_ref, parent, tokens).unwrap_or(right_operand.range()); let right_operand_expr = locator.slice(right_operand_range); let target_expr = locator.slice(target); diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs index 0ed569eef8..21a4643f39 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs @@ -75,12 +75,7 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa let mut diagnostic = checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range()); diagnostic.set_fix(Fix::applicable_edit( - add_argument( - "check=False", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ), + add_argument("check=False", &call.arguments, checker.tokens()), // If the function call contains `**kwargs`, mark the fix as unsafe. if call .arguments diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index 7d9cd12506..316d2e7f54 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -1,8 +1,7 @@ use std::fmt::{Display, Formatter}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::name::QualifiedName; -use ruff_python_ast::{self as ast, Expr}; +use ruff_python_ast::{self as ast, Expr, name::QualifiedName}; use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::typing; use ruff_text_size::{Ranged, TextRange}; @@ -193,8 +192,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix { })) ), &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), )) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs index b0d273b7c4..02f8cdd1e8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs @@ -204,7 +204,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD arguments, Parentheses::Remove, checker.source(), - checker.comment_ranges(), + checker.tokens(), )?; Ok(Fix::unsafe_edits( Edit::insertion(type_params.to_string(), name.end()), diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs index 43e3ec8536..6b10c3bc07 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef}; use ruff_text_size::{Ranged, TextRange}; @@ -261,11 +261,11 @@ fn create_diagnostic( type_alias_kind: TypeAliasKind, ) { let source = checker.source(); + let tokens = checker.tokens(); let comment_ranges = checker.comment_ranges(); let range_with_parentheses = - parenthesized_range(value.into(), stmt.into(), comment_ranges, source) - .unwrap_or(value.range()); + parenthesized_range(value.into(), stmt.into(), tokens).unwrap_or(value.range()); let content = format!( "type {name}{type_params} = {value}", diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 7c5dd2f027..6ce403647d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -1,9 +1,8 @@ use anyhow::Result; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{self as ast, Keyword}; +use ruff_python_ast::{self as ast, Keyword, token::Tokens}; use ruff_python_semantic::Modules; -use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -104,7 +103,7 @@ pub(crate) fn replace_stdout_stderr(checker: &Checker, call: &ast::ExprCall) { stderr, call, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) }); } @@ -117,7 +116,7 @@ fn generate_fix( stderr: &Keyword, call: &ast::ExprCall, source: &str, - comment_ranges: &CommentRanges, + tokens: &Tokens, ) -> Result { let (first, second) = if stdout.start() < stderr.start() { (stdout, stderr) @@ -132,7 +131,7 @@ fn generate_fix( &call.arguments, Parentheses::Preserve, source, - comment_ranges, + tokens, )?], )) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 97000fa75f..b7b13c7b66 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -78,7 +78,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index 01cfe4fb03..792365042f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -188,7 +188,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -206,7 +206,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -231,7 +231,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -249,7 +249,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs index 75bbe20eca..20d3f64461 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs @@ -70,7 +70,7 @@ pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtCl arguments, Parentheses::Remove, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), )?; let range = edit.range(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs index 4a5789c78f..a1b0d900f8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance(checker: &Checker, class_def: &ast::Stm arguments, Parentheses::Remove, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), )?; let range = edit.range(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs index 7fec2b7d79..55f2afc89a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::Ranged; @@ -139,13 +139,8 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) { let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range()); let contents = checker.locator().slice( - parenthesized_range( - iter.as_ref().into(), - stmt_for.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(iter.range()), + parenthesized_range(iter.as_ref().into(), stmt_for.into(), checker.tokens()) + .unwrap_or(iter.range()), ); let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) { format!("yield from ({contents})") diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index 0a09d70aba..a6871f0497 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use ruff_python_ast::PythonVersion; -use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range}; +use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range}; use ruff_python_codegen::Generator; use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; @@ -330,12 +330,8 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>( let locator = checker.locator(); let iter = for_stmt.iter.as_ref(); - let original_parenthesized_range = parenthesized_range( - iter.into(), - for_stmt.into(), - checker.comment_ranges(), - checker.source(), - ); + let original_parenthesized_range = + parenthesized_range(iter.into(), for_stmt.into(), checker.tokens()); if let Some(range) = original_parenthesized_range { return Cow::Borrowed(locator.slice(range)); diff --git a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs index 3622148fbf..b2b3193d9e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{ Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp, Number, Operator, PythonVersion, UnaryOp, @@ -112,8 +112,7 @@ pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) { let value_full_range = parenthesized_range( replace_time_zone.date.into(), replace_time_zone.parent.into(), - checker.comment_ranges(), - checker.source(), + checker.tokens(), ) .unwrap_or(replace_time_zone.date.range()); diff --git a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs index fa660587ef..f4f4d1f7b7 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs @@ -5,8 +5,7 @@ use ruff_python_ast as ast; use ruff_python_ast::Expr; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::parenthesize::parenthesized_range; -use ruff_python_trivia::CommentRanges; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_text_size::Ranged; use crate::Locator; @@ -76,8 +75,8 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex Edit::range_replacement( format!( "{} or {}", - parenthesize_test(test, if_expr, checker.comment_ranges(), checker.locator()), - parenthesize_test(orelse, if_expr, checker.comment_ranges(), checker.locator()), + parenthesize_test(test, if_expr, checker.tokens(), checker.locator()), + parenthesize_test(orelse, if_expr, checker.tokens(), checker.locator()), ), if_expr.range(), ), @@ -99,15 +98,10 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex fn parenthesize_test<'a>( expr: &Expr, if_expr: &ast::ExprIf, - comment_ranges: &CommentRanges, + tokens: &Tokens, locator: &Locator<'a>, ) -> Cow<'a, str> { - if let Some(range) = parenthesized_range( - expr.into(), - if_expr.into(), - comment_ranges, - locator.contents(), - ) { + if let Some(range) = parenthesized_range(expr.into(), if_expr.into(), tokens) { Cow::Borrowed(locator.slice(range)) } else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) { Cow::Owned(format!("({})", locator.slice(expr.range()))) diff --git a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs index a6ea1eb570..943a013cbb 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::Applicability; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{Comprehension, Expr, StmtFor}; use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing::is_io_base_expr; @@ -104,8 +104,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) { let deletion_range = if let Some(parenthesized_range) = parenthesized_range( expr_attr.value.as_ref().into(), expr_attr.into(), - checker.comment_ranges(), - checker.source(), + checker.tokens(), ) { expr_call.range().add_start(parenthesized_range.len()) } else { diff --git a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs index a54bd261d1..35774cde28 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs @@ -1,7 +1,7 @@ use anyhow::Result; use ruff_diagnostics::Applicability; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Number}; use ruff_text_size::Ranged; @@ -152,13 +152,8 @@ fn generate_fix(checker: &Checker, call: &ast::ExprCall, base: Base, arg: &Expr) checker.semantic(), )?; - let arg_range = parenthesized_range( - arg.into(), - call.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(arg.range()); + let arg_range = + parenthesized_range(arg.into(), call.into(), checker.tokens()).unwrap_or(arg.range()); let arg_str = checker.locator().slice(arg_range); Ok(Fix::applicable_edits( 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 1df2fdde78..04b00f07f7 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 @@ -95,7 +95,7 @@ pub(crate) fn single_item_membership_test( &[membership_test.replacement_op()], std::slice::from_ref(item), expr.into(), - checker.comment_ranges(), + checker.tokens(), checker.source(), ), expr.range(), diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs index 4178217718..7b023de830 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs @@ -163,7 +163,7 @@ fn convert_type_vars( class_arguments, Parentheses::Remove, source, - checker.comment_ranges(), + checker.tokens(), )?; let replace_type_params = Edit::range_replacement(new_type_params.to_string(), type_params.range); diff --git a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs index ea792ce97c..70ef8cb6d4 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs @@ -3,8 +3,8 @@ use anyhow::Result; use ast::Keyword; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_constant; +use ruff_python_ast::token::Tokens; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::Locator; @@ -108,9 +108,8 @@ pub(crate) fn default_factory_kwarg(checker: &Checker, call: &ast::ExprCall) { }, call.range(), ); - diagnostic.try_set_fix(|| { - convert_to_positional(call, keyword, checker.locator(), checker.comment_ranges()) - }); + diagnostic + .try_set_fix(|| convert_to_positional(call, keyword, checker.locator(), checker.tokens())); } /// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`). @@ -136,7 +135,7 @@ fn convert_to_positional( call: &ast::ExprCall, default_factory: &Keyword, locator: &Locator, - comment_ranges: &CommentRanges, + tokens: &Tokens, ) -> Result { if call.arguments.len() == 1 { // Ex) `defaultdict(default_factory=list)` @@ -153,7 +152,7 @@ fn convert_to_positional( &call.arguments, Parentheses::Preserve, locator.contents(), - comment_ranges, + tokens, )?; // Second, insert the value as the first positional argument. 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 a19a9c451a..de3c072bdd 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 @@ -128,7 +128,7 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(|edit| Fix::applicable_edit(edit, applicability)) }); 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 7433f63f2b..6e2351aafb 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 @@ -1,6 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -77,14 +77,7 @@ pub(crate) fn parenthesize_chained_logical_operators(checker: &Checker, expr: &a ) => { let locator = checker.locator(); let source_range = bool_op.range(); - if parenthesized_range( - bool_op.into(), - expr.into(), - checker.comment_ranges(), - locator.contents(), - ) - .is_none() - { + if parenthesized_range(bool_op.into(), expr.into(), checker.tokens()).is_none() { let new_source = format!("({})", locator.slice(source_range)); let edit = Edit::range_replacement(new_source, source_range); checker diff --git a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs index 36941a98c9..2ec2472262 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs @@ -2,7 +2,7 @@ use anyhow::Context; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_semantic::{Scope, ScopeKind}; use ruff_python_trivia::{indentation_at_offset, textwrap}; use ruff_source_file::LineRanges; @@ -159,8 +159,7 @@ fn use_initvar( let default_loc = parenthesized_range( default.into(), parameter_with_default.into(), - checker.comment_ranges(), - checker.source(), + checker.tokens(), ) .unwrap_or(default.range()); 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 d4e062ad71..a38fd2fd6d 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 @@ -2,7 +2,7 @@ use anyhow::Result; use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; @@ -116,13 +116,8 @@ fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) - )?; let iterable = checker.locator().slice( - parenthesized_range( - iterable.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(iterable.range()), + parenthesized_range(iterable.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or(iterable.range()), ); Ok(Fix::unsafe_edits( 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 e9ed8d31bb..79881b8abc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs @@ -1,7 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PythonVersion; use ruff_python_ast::token::TokenKind; -use ruff_python_ast::{Expr, ExprCall, parenthesize::parenthesized_range}; +use ruff_python_ast::{Expr, ExprCall, token::parenthesized_range}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -124,13 +124,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op let mut remove_zip = vec![]; - let full_zip_range = parenthesized_range( - zip.into(), - starmap.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(zip.range()); + let full_zip_range = + parenthesized_range(zip.into(), starmap.into(), checker.tokens()).unwrap_or(zip.range()); // Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple. remove_zip.push(Edit::range_deletion(TextRange::new( @@ -138,13 +133,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op zip.start(), ))); - let full_zip_func_range = parenthesized_range( - (&zip.func).into(), - zip.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(zip.func.range()); + let full_zip_func_range = parenthesized_range((&zip.func).into(), zip.into(), checker.tokens()) + .unwrap_or(zip.func.range()); // Delete the `zip` callee remove_zip.push(Edit::range_deletion(full_zip_func_range)); diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index b3c34c29e0..453aa07801 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_python_ast::{Arguments, Expr, ExprCall}; use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; @@ -86,6 +86,7 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) { applicability, checker.semantic(), checker.locator(), + checker.tokens(), checker.comment_ranges(), checker.source(), ); @@ -95,27 +96,26 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) { } /// Creates a fix that replaces `int(expression)` with `expression`. +#[allow(clippy::too_many_arguments)] fn unwrap_int_expression( call: &ExprCall, argument: &Expr, applicability: Applicability, semantic: &SemanticModel, locator: &Locator, + tokens: &Tokens, comment_ranges: &CommentRanges, source: &str, ) -> Fix { - let content = if let Some(range) = parenthesized_range( - argument.into(), - (&call.arguments).into(), - comment_ranges, - source, - ) { + let content = if let Some(range) = + parenthesized_range(argument.into(), (&call.arguments).into(), tokens) + { locator.slice(range).to_string() } else { let parenthesize = semantic.current_expression_parent().is_some() || argument.is_named_expr() || locator.count_lines(argument.range()) > 0; - if parenthesize && !has_own_parentheses(argument, comment_ranges, source) { + if parenthesize && !has_own_parentheses(argument, tokens, source) { format!("({})", locator.slice(argument.range())) } else { locator.slice(argument.range()).to_string() @@ -255,7 +255,7 @@ fn round_applicability(arguments: &Arguments, semantic: &SemanticModel) -> Optio } /// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`). -fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str) -> bool { +fn has_own_parentheses(expr: &Expr, tokens: &Tokens, source: &str) -> bool { match expr { Expr::ListComp(_) | Expr::SetComp(_) @@ -276,14 +276,10 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str // f // (10) // ``` - let func_end = parenthesized_range( - call_expr.func.as_ref().into(), - call_expr.into(), - comment_ranges, - source, - ) - .unwrap_or(call_expr.func.range()) - .end(); + let func_end = + parenthesized_range(call_expr.func.as_ref().into(), call_expr.into(), tokens) + .unwrap_or(call_expr.func.range()) + .end(); lines_after_ignoring_trivia(func_end, source) == 0 } Expr::Subscript(subscript_expr) => { @@ -291,8 +287,7 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str let subscript_end = parenthesized_range( subscript_expr.value.as_ref().into(), subscript_expr.into(), - comment_ranges, - source, + tokens, ) .unwrap_or(subscript_expr.value.range()) .end(); diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs index 7c13fb3d1c..502391dcf2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -108,22 +108,12 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) { format!( "{}.get({})", checker.locator().slice( - parenthesized_range( - obj_right.into(), - right.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(obj_right.range()) + parenthesized_range(obj_right.into(), right.into(), checker.tokens(),) + .unwrap_or(obj_right.range()) ), checker.locator().slice( - parenthesized_range( - key_right.into(), - right.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(key_right.range()) + parenthesized_range(key_right.into(), right.into(), checker.tokens(),) + .unwrap_or(key_right.range()) ), ), expr.range(), 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 fd31765715..b322d57fd6 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 @@ -2,7 +2,7 @@ use ruff_diagnostics::{Applicability, Edit}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_empty_f_string; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -140,31 +140,19 @@ fn fix_unnecessary_literal_in_deque( // call. otherwise, we only delete the `iterable` argument and leave the others untouched. let edit = if let Some(maxlen) = maxlen { let deque_name = checker.locator().slice( - parenthesized_range( - deque.func.as_ref().into(), - deque.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(deque.func.range()), + parenthesized_range(deque.func.as_ref().into(), deque.into(), checker.tokens()) + .unwrap_or(deque.func.range()), ); let len_str = checker.locator().slice(maxlen); let deque_str = format!("{deque_name}(maxlen={len_str})"); Edit::range_replacement(deque_str, deque.range) } else { - let range = parenthesized_range( - iterable.value().into(), - (&deque.arguments).into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(iterable.range()); remove_argument( - &range, + &iterable, &deque.arguments, Parentheses::Preserve, checker.source(), - checker.comment_ranges(), + checker.tokens(), )? }; let has_comments = checker.comment_ranges().intersects(edit.range()); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 4879e04780..9680d03ec3 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -3,13 +3,14 @@ use std::path::Path; use rustc_hash::FxHashMap; -use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer, indentation_at_offset}; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, indentation_at_offset}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::name::{Name, QualifiedName, QualifiedNameBuilder}; -use crate::parenthesize::parenthesized_range; use crate::statement_visitor::StatementVisitor; +use crate::token::Tokens; +use crate::token::parenthesized_range; use crate::visitor::Visitor; use crate::{ self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral, @@ -1474,7 +1475,7 @@ pub fn generate_comparison( ops: &[CmpOp], comparators: &[Expr], parent: AnyNodeRef, - comment_ranges: &CommentRanges, + tokens: &Tokens, source: &str, ) -> String { let start = left.start(); @@ -1483,8 +1484,7 @@ pub fn generate_comparison( // Add the left side of the comparison. contents.push_str( - &source[parenthesized_range(left.into(), parent, comment_ranges, source) - .unwrap_or(left.range())], + &source[parenthesized_range(left.into(), parent, tokens).unwrap_or(left.range())], ); for (op, comparator) in ops.iter().zip(comparators) { @@ -1504,7 +1504,7 @@ pub fn generate_comparison( // Add the right side of the comparison. contents.push_str( - &source[parenthesized_range(comparator.into(), parent, comment_ranges, source) + &source[parenthesized_range(comparator.into(), parent, tokens) .unwrap_or(comparator.range())], ); } From c51727708a3814ed6b32bb000fbe2d692d85e488 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:23:10 -0500 Subject: [PATCH 22/36] Enable `--document-private-items` for `ruff_python_formatter` (#21903) --- .github/workflows/ci.yaml | 2 +- .../src/comments/debug.rs | 2 +- .../ruff_python_formatter/src/comments/map.rs | 2 +- .../src/comments/node_key.rs | 3 +- .../src/comments/placement.rs | 8 ++--- .../src/comments/visitor.rs | 5 +-- crates/ruff_python_formatter/src/context.rs | 2 +- .../src/expression/binary_like.rs | 6 ++-- .../src/expression/parentheses.rs | 14 ++++---- .../ruff_python_formatter/src/pattern/mod.rs | 5 +-- .../src/pattern/pattern_arguments.rs | 5 +-- crates/ruff_python_formatter/src/range.rs | 34 ++++++++++++------- .../src/statement/stmt_assign.rs | 15 ++++---- .../src/statement/stmt_with.rs | 6 ++-- .../src/string/docstring.rs | 2 +- .../src/string/normalize.rs | 5 +-- .../src/type_param/type_params.rs | 2 +- crates/ruff_python_formatter/src/verbatim.rs | 5 +-- 18 files changed, 70 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 993ecfac97..b218a2e99b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -298,7 +298,7 @@ jobs: # sync, not just public items. Eventually we should do this for all # crates; for now add crates here as they are warning-clean to prevent # regression. - - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items + - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db -p ruff_python_formatter --document-private-items env: # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). RUSTDOCFLAGS: "-D warnings" diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index 3e4ae7c6bd..0adbba24f3 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -36,7 +36,7 @@ impl Debug for DebugComment<'_> { } } -/// Pretty-printed debug representation of [`Comments`]. +/// Pretty-printed debug representation of [`Comments`](super::Comments). pub(crate) struct DebugComments<'a> { comments: &'a CommentsMap<'a>, source_code: SourceCode<'a>, diff --git a/crates/ruff_python_formatter/src/comments/map.rs b/crates/ruff_python_formatter/src/comments/map.rs index 3f6d621a59..8d25f4f8a7 100644 --- a/crates/ruff_python_formatter/src/comments/map.rs +++ b/crates/ruff_python_formatter/src/comments/map.rs @@ -504,7 +504,7 @@ impl InOrderEntry { #[derive(Clone, Debug)] struct OutOfOrderEntry { - /// Index into the [`MultiMap::out_of_order`] vector at which offset the leading vec is stored. + /// Index into the [`MultiMap::out_of_order_parts`] vector at which offset the leading vec is stored. leading_index: usize, _count: Count, } diff --git a/crates/ruff_python_formatter/src/comments/node_key.rs b/crates/ruff_python_formatter/src/comments/node_key.rs index ec15ced488..115a751150 100644 --- a/crates/ruff_python_formatter/src/comments/node_key.rs +++ b/crates/ruff_python_formatter/src/comments/node_key.rs @@ -2,7 +2,8 @@ use ruff_python_ast::AnyNodeRef; use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; -/// Used as key into the [`MultiMap`] storing the comments per node by [`Comments`]. +/// Used as key into the [`MultiMap`](super::MultiMap) storing the comments per node by +/// [`Comments`](super::Comments). /// /// Implements equality and hashing based on the address of the [`AnyNodeRef`] to get fast and cheap /// hashing/equality comparison. diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 76449285be..ff38d7a100 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1974,8 +1974,8 @@ fn handle_unary_op_comment<'a>( /// ) /// ``` /// -/// The comment will be attached to the [`Arguments`] node as a dangling comment, to ensure -/// that it remains on the same line as open parenthesis. +/// The comment will be attached to the [`Arguments`](ast::Arguments) node as a dangling comment, to +/// ensure that it remains on the same line as open parenthesis. /// /// Similarly, given: /// ```python @@ -1984,8 +1984,8 @@ fn handle_unary_op_comment<'a>( /// ] = ... /// ``` /// -/// The comment will be attached to the [`TypeParams`] node as a dangling comment, to ensure -/// that it remains on the same line as open bracket. +/// The comment will be attached to the [`TypeParams`](ast::TypeParams) node as a dangling comment, +/// to ensure that it remains on the same line as open bracket. fn handle_bracketed_end_of_line_comment<'a>( comment: DecoratedComment<'a>, source: &str, diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index b9670a3ee8..1aad7e273d 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -174,7 +174,8 @@ impl<'ast> SourceOrderVisitor<'ast> for CommentsVisitor<'ast, '_> { /// A comment decorated with additional information about its surrounding context in the source document. /// -/// Used by [`CommentStyle::place_comment`] to determine if this should become a [leading](self#leading-comments), [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment. +/// Used by [`place_comment`] to determine if this should become a [leading](self#leading-comments), +/// [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment. #[derive(Debug, Clone)] pub(crate) struct DecoratedComment<'a> { enclosing: AnyNodeRef<'a>, @@ -465,7 +466,7 @@ pub(super) enum CommentPlacement<'a> { /// /// [`preceding_node`]: DecoratedComment::preceding_node /// [`following_node`]: DecoratedComment::following_node - /// [`enclosing_node`]: DecoratedComment::enclosing_node_id + /// [`enclosing_node`]: DecoratedComment::enclosing_node /// [trailing comment]: self#trailing-comments /// [leading comment]: self#leading-comments /// [dangling comment]: self#dangling-comments diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 239edc8d5b..8eaf52ee35 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -166,7 +166,7 @@ impl InterpolatedStringState { } } - /// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`]. + /// Returns `true` if the interpolated string state is [`Self::NestedInterpolatedElement`]. pub(crate) fn is_nested(self) -> bool { matches!(self, Self::NestedInterpolatedElement(..)) } diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index 5d86608452..f1da88299d 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -1095,9 +1095,9 @@ impl OperandIndex { } } - /// Returns the index of the operand's right operator. The method always returns an index - /// even if the operand has no right operator. Use [`BinaryCallChain::get_operator`] to test if - /// the operand has a right operator. + /// Returns the index of the operand's right operator. The method always returns an index even + /// if the operand has no right operator. Use [`FlatBinaryExpressionSlice::get_operator`] to + /// test if the operand has a right operator. fn right_operator(self) -> OperatorIndex { OperatorIndex::new(self.0 + 1) } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index a76f8a0aec..18b9b274c1 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -56,18 +56,20 @@ pub(crate) enum Parenthesize { /// Adding parentheses is desired to prevent the comments from wandering. IfRequired, - /// Same as [`Self::IfBreaks`] except that it uses [`parenthesize_if_expands`] for expressions - /// with the layout [`NeedsParentheses::BestFit`] which is used by non-splittable - /// expressions like literals, name, and strings. + /// Same as [`Self::IfBreaks`] except that it uses + /// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for expressions with + /// the layout [`OptionalParentheses::BestFit`] which is used by non-splittable expressions like + /// literals, name, and strings. /// /// Use this layout over `IfBreaks` when there's a sequence of `maybe_parenthesize_expression` /// in a single logical-line and you want to break from right-to-left. Use `IfBreaks` for the /// first expression and `IfBreaksParenthesized` for the rest. IfBreaksParenthesized, - /// Same as [`Self::IfBreaksParenthesized`] but uses [`parenthesize_if_expands`] for nested - /// [`maybe_parenthesized_expression`] calls unlike other layouts that always omit parentheses - /// when outer parentheses are present. + /// Same as [`Self::IfBreaksParenthesized`] but uses + /// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for nested + /// [`maybe_parenthesized_expression`](crate::expression::maybe_parenthesize_expression) calls + /// unlike other layouts that always omit parentheses when outer parentheses are present. IfBreaksParenthesizedNested, } diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index a379aeb849..ac763a41ff 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -214,8 +214,9 @@ impl Format> for MaybeParenthesizePattern<'_> { } } -/// This function is very similar to [`can_omit_optional_parentheses`] with the only difference that it is for patterns -/// and not expressions. +/// This function is very similar to +/// [`can_omit_optional_parentheses`](crate::expression::can_omit_optional_parentheses) +/// with the only difference that it is for patterns and not expressions. /// /// The base idea of the omit optional parentheses layout is to prefer using parentheses of sub-patterns /// when splitting the pattern over introducing new patterns. For example, prefer splitting the sequence pattern in diff --git a/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs b/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs index 94d7448226..d82a9756e0 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs @@ -72,8 +72,9 @@ impl FormatNodeRule for FormatPatternArguments { } } -/// Returns `true` if the pattern (which is the only argument to a [`PatternMatchClass`]) is -/// parenthesized. Used to avoid falsely assuming that `x` is parenthesized in cases like: +/// Returns `true` if the pattern (which is the only argument to a +/// [`PatternMatchClass`](ruff_python_ast::PatternMatchClass)) is parenthesized. +/// Used to avoid falsely assuming that `x` is parenthesized in cases like: /// ```python /// case Point2D(x): ... /// ``` diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 436c1a12c2..ccc7a51aba 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -23,7 +23,8 @@ use crate::{FormatModuleError, PyFormatOptions, format_module_source}; /// /// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger. /// Some cases in which the returned range is larger than `range` are: -/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] and [`IndentWidth`]. +/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] +/// and [`IndentWidth`](ruff_formatter::IndentWidth). /// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line. /// * `range` falls on a single line body. /// @@ -129,16 +130,19 @@ pub fn format_range( /// b) formatting a sub-expression has fewer split points than formatting the entire expressions. /// /// ### Possible docstrings -/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing suite instead -/// so that the formatter's docstring detection in [`FormatSuite`] correctly detects and formats the docstrings. +/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing +/// suite instead so that the formatter's docstring detection in +/// [`FormatSuite`](crate::statement::suite::FormatSuite) correctly detects and formats the +/// docstrings. /// /// ### Compound statements with a simple statement body /// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter -/// must run [`FormatClauseBody`] to determine if the body should be collapsed or not. +/// must run `FormatClauseBody` to determine if the body should be collapsed or not. /// /// ### Incorrectly indented code -/// Code that uses indentations that don't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search, -/// because formatting such nodes on their own can lead to indentation mismatch with its sibling nodes. +/// Code that uses indentations that don't match the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search, because formatting +/// such nodes on their own can lead to indentation mismatch with its sibling nodes. /// /// ## Suppression comments /// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the @@ -279,13 +283,15 @@ enum EnclosingNode<'a> { /// /// ## Compound statements with simple statement bodies /// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header -/// with the body. This ensures that the formatter runs [`FormatClauseBody`] that determines if the body should be indented.s +/// with the body. This ensures that the formatter runs `FormatClauseBody` that determines if the body should be indented. /// /// ## Non-standard indentation -/// Node's that use an indentation that doesn't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search. -/// This is because the formatter always uses the configured [`IndentStyle`] and [`IndentWidth`], resulting in the -/// formatted nodes using a different indentation than the unformatted sibling nodes. This would be tolerable -/// in non whitespace sensitive languages like JavaScript but results in lexical errors in Python. +/// Nodes that use an indentation that doesn't match the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search. This is because the +/// formatter always uses the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth), resulting in the formatted nodes using a different +/// indentation than the unformatted sibling nodes. This would be tolerable in non whitespace +/// sensitive languages like JavaScript but results in lexical errors in Python. /// /// ## Implementation /// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because @@ -713,9 +719,11 @@ impl Format> for FormatEnclosingNode<'_> { } } -/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] and [`IndentWidth`]. +/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] +/// and [`IndentWidth`](ruff_formatter::IndentWidth). /// -/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and [`IndentWidth`]. +/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth). /// /// # Panics /// If `offset` is outside of `source`. diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index b9fbe6b7a3..5a16e5e8bf 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -184,7 +184,7 @@ impl Format> for FormatTargetWithEqualOperator<'_> { /// No parentheses are added for `short` because it fits into the configured line length, regardless of whether /// the comment exceeds the line width or not. /// -/// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because +/// This logic isn't implemented in `place_comment` by associating trailing statement comments to the expression because /// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement. #[derive(Debug)] pub(super) enum FormatStatementsLastExpression<'a> { @@ -202,8 +202,8 @@ pub(super) enum FormatStatementsLastExpression<'a> { /// ] = some_long_value /// ``` /// - /// This layout is preferred over [`RightToLeft`] if the left is unsplittable (single keyword like `return` or a Name) - /// because it has better performance characteristics. + /// This layout is preferred over [`Self::RightToLeft`] if the left is unsplittable (single + /// keyword like `return` or a Name) because it has better performance characteristics. LeftToRight { /// The right side of an assignment or the value returned in a return statement. value: &'a Expr, @@ -1083,11 +1083,10 @@ impl Format> for InterpolatedString<'_> { /// For legibility, we discuss only the case of f-strings below, but the /// same comments apply to t-strings. /// -/// This is just a wrapper around [`FormatFString`] while considering a special -/// case when the f-string is at an assignment statement's value position. -/// This is necessary to prevent an instability where an f-string contains a -/// multiline expression and the f-string fits on the line, but only when it's -/// surrounded by parentheses. +/// This is just a wrapper around [`FormatFString`](crate::other::f_string::FormatFString) while +/// considering a special case when the f-string is at an assignment statement's value position. +/// This is necessary to prevent an instability where an f-string contains a multiline expression +/// and the f-string fits on the line, but only when it's surrounded by parentheses. /// /// ```python /// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 3ef8e52a23..4e2c6bc876 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -177,8 +177,10 @@ enum WithItemsLayout<'a> { /// ... /// ``` /// - /// In this case, use [`maybe_parenthesize_expression`] to format the context expression - /// to get the exact same formatting as when formatting an expression in any other clause header. + /// In this case, use + /// [`maybe_parenthesize_expression`](crate::expression::maybe_parenthesize_expression) to + /// format the context expression to get the exact same formatting as when formatting an + /// expression in any other clause header. /// /// Only used for Python 3.9+ /// diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index ad357fc65e..c4554175d5 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -783,7 +783,7 @@ enum CodeExampleKind<'src> { /// /// Documentation describing doctests and how they're recognized can be /// found as part of the Python standard library: - /// https://docs.python.org/3/library/doctest.html. + /// . /// /// (You'll likely need to read the [regex matching] used internally by the /// doctest module to determine more precisely how it works.) diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index fabd10a029..1500737950 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -38,8 +38,9 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { /// it can't because the string contains the preferred quotes OR /// it leads to more escaping. /// - /// Note: If you add more cases here where we return `QuoteStyle::Preserve`, - /// make sure to also add them to [`FormatImplicitConcatenatedStringFlat::new`]. + /// Note: If you add more cases here where we return `QuoteStyle::Preserve`, make sure to also + /// add them to + /// [`FormatImplicitConcatenatedStringFlat::new`](crate::string::implicit::FormatImplicitConcatenatedStringFlat::new). pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle { let preferred_quote_style = self .preferred_quote_style diff --git a/crates/ruff_python_formatter/src/type_param/type_params.rs b/crates/ruff_python_formatter/src/type_param/type_params.rs index e243dc6006..50c2cc5770 100644 --- a/crates/ruff_python_formatter/src/type_param/type_params.rs +++ b/crates/ruff_python_formatter/src/type_param/type_params.rs @@ -9,7 +9,7 @@ use crate::prelude::*; #[derive(Default)] pub struct FormatTypeParams; -/// Formats a sequence of [`TypeParam`] nodes. +/// Formats a sequence of [`TypeParam`](ruff_python_ast::TypeParam) nodes. impl FormatNodeRule for FormatTypeParams { fn fmt_fields(&self, item: &TypeParams, f: &mut PyFormatter) -> FormatResult<()> { // A dangling comment indicates a comment on the same line as the opening bracket, e.g.: diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index e0bbf00ad6..3dc78a92b7 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -679,8 +679,9 @@ impl Indentation { /// Returns `true` for a space or tab character. /// -/// This is different than [`is_python_whitespace`] in that it returns `false` for a form feed character. -/// Form feed characters are excluded because they should be preserved in the suppressed output. +/// This is different than [`is_python_whitespace`](ruff_python_trivia::is_python_whitespace) in +/// that it returns `false` for a form feed character. Form feed characters are excluded because +/// they should be preserved in the suppressed output. const fn is_indent_whitespace(c: char) -> bool { matches!(c, ' ' | '\t') } From c1c45a6a131d9e2f6aed2e39a1c4fee04d061a3e Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 9 Dec 2025 13:37:48 -0500 Subject: [PATCH 23/36] [ty] Remove `all_` prefix from some routines on UseDefMap These routines don't return *all* symbols/members, but rather, only *for* a particular scope. We do specifically want to add some routines that return *all* symbols/members, and this naming scheme made that confusing. It was also inconsistent with other routines like `all_end_of_scope_symbol_declarations` which *do* return *all* symbols. --- crates/ty_python_semantic/src/place.rs | 4 ++-- .../ty_python_semantic/src/semantic_index.rs | 7 ++----- .../src/semantic_index/use_def.rs | 20 +++++++++---------- crates/ty_python_semantic/src/types/class.rs | 2 +- crates/ty_python_semantic/src/types/enums.rs | 2 +- .../src/types/ide_support.rs | 20 +++++++++---------- .../src/types/infer/builder.rs | 2 +- .../ty_python_semantic/src/types/overrides.rs | 2 +- 8 files changed, 28 insertions(+), 31 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 98f7a2b8e4..a1319a3250 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -783,7 +783,7 @@ pub(crate) fn place_by_id<'db>( let declarations = match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_declarations(place_id), - ConsideredDefinitions::AllReachable => use_def.all_reachable_declarations(place_id), + ConsideredDefinitions::AllReachable => use_def.reachable_declarations(place_id), }; let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport) @@ -791,7 +791,7 @@ pub(crate) fn place_by_id<'db>( let all_considered_bindings = || match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id), - ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id), + ConsideredDefinitions::AllReachable => use_def.reachable_bindings(place_id), }; // If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index f4ab765f08..a38b0a7ded 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -113,10 +113,7 @@ pub(crate) fn attribute_assignments<'db, 's>( let place_table = index.place_table(function_scope_id); let member = place_table.member_id_by_instance_attribute_name(name)?; let use_def = &index.use_def_maps[function_scope_id]; - Some(( - use_def.all_reachable_member_bindings(member), - function_scope_id, - )) + Some((use_def.reachable_member_bindings(member), function_scope_id)) }) } @@ -138,7 +135,7 @@ pub(crate) fn attribute_declarations<'db, 's>( let member = place_table.member_id_by_instance_attribute_name(name)?; let use_def = &index.use_def_maps[function_scope_id]; Some(( - use_def.all_reachable_member_declarations(member), + use_def.reachable_member_declarations(member), function_scope_id, )) }) diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index a7c7520806..5683fc736b 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -453,17 +453,17 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn all_reachable_bindings( + pub(crate) fn reachable_bindings( &self, place: ScopedPlaceId, ) -> BindingWithConstraintsIterator<'_, 'db> { match place { - ScopedPlaceId::Symbol(symbol) => self.all_reachable_symbol_bindings(symbol), - ScopedPlaceId::Member(member) => self.all_reachable_member_bindings(member), + ScopedPlaceId::Symbol(symbol) => self.reachable_symbol_bindings(symbol), + ScopedPlaceId::Member(member) => self.reachable_member_bindings(member), } } - pub(crate) fn all_reachable_symbol_bindings( + pub(crate) fn reachable_symbol_bindings( &self, symbol: ScopedSymbolId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -471,7 +471,7 @@ impl<'db> UseDefMap<'db> { self.bindings_iterator(bindings, BoundnessAnalysis::AssumeBound) } - pub(crate) fn all_reachable_member_bindings( + pub(crate) fn reachable_member_bindings( &self, symbol: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -547,7 +547,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } - pub(crate) fn all_reachable_symbol_declarations( + pub(crate) fn reachable_symbol_declarations( &self, symbol: ScopedSymbolId, ) -> DeclarationsIterator<'_, 'db> { @@ -555,7 +555,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) } - pub(crate) fn all_reachable_member_declarations( + pub(crate) fn reachable_member_declarations( &self, member: ScopedMemberId, ) -> DeclarationsIterator<'_, 'db> { @@ -563,13 +563,13 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) } - pub(crate) fn all_reachable_declarations( + pub(crate) fn reachable_declarations( &self, place: ScopedPlaceId, ) -> DeclarationsIterator<'_, 'db> { match place { - ScopedPlaceId::Symbol(symbol) => self.all_reachable_symbol_declarations(symbol), - ScopedPlaceId::Member(member) => self.all_reachable_member_declarations(member), + ScopedPlaceId::Symbol(symbol) => self.reachable_symbol_declarations(symbol), + ScopedPlaceId::Member(member) => self.reachable_member_declarations(member), } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 71ae73f1b1..32962ea128 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3457,7 +3457,7 @@ impl<'db> ClassLiteral<'db> { .symbol_id(&method_def.node(&module).name) .unwrap(); class_map - .all_reachable_symbol_bindings(method_place) + .reachable_symbol_bindings(method_place) .find_map(|bind| { (bind.binding.is_defined_and(|def| def == method)) .then(|| class_map.binding_reachability(db, &bind)) diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 9a91d1da3d..fcc8552b52 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -76,7 +76,7 @@ pub(crate) fn enum_metadata<'db>( let mut auto_counter = 0; let ignored_names: Option> = if let Some(ignore) = table.symbol_id("_ignore_") { - let ignore_bindings = use_def_map.all_reachable_symbol_bindings(ignore); + let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); let ignore_place = place_from_bindings(db, ignore_bindings).place; match ignore_place { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index d32eba2ace..5162e8a13d 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -86,9 +86,9 @@ pub fn definitions_for_name<'db>( if let Some(global_symbol_id) = global_place_table.symbol_id(name_str) { let global_use_def_map = crate::semantic_index::use_def_map(db, global_scope_id); let global_bindings = - global_use_def_map.all_reachable_symbol_bindings(global_symbol_id); + global_use_def_map.reachable_symbol_bindings(global_symbol_id); let global_declarations = - global_use_def_map.all_reachable_symbol_declarations(global_symbol_id); + global_use_def_map.reachable_symbol_declarations(global_symbol_id); for binding in global_bindings { if let Some(def) = binding.binding.definition() { @@ -114,8 +114,8 @@ pub fn definitions_for_name<'db>( let use_def_map = index.use_def_map(scope_id); // Get all definitions (both bindings and declarations) for this place - let bindings = use_def_map.all_reachable_symbol_bindings(symbol_id); - let declarations = use_def_map.all_reachable_symbol_declarations(symbol_id); + let bindings = use_def_map.reachable_symbol_bindings(symbol_id); + let declarations = use_def_map.reachable_symbol_declarations(symbol_id); for binding in bindings { if let Some(def) = binding.binding.definition() { @@ -294,7 +294,7 @@ pub fn definitions_for_attribute<'db>( let use_def = use_def_map(db, class_scope); // Check declarations first - for decl in use_def.all_reachable_symbol_declarations(place_id) { + for decl in use_def.reachable_symbol_declarations(place_id) { if let Some(def) = decl.declaration.definition() { resolved.extend(resolve_definition( db, @@ -307,7 +307,7 @@ pub fn definitions_for_attribute<'db>( } // If no declarations found, check bindings - for binding in use_def.all_reachable_symbol_bindings(place_id) { + for binding in use_def.reachable_symbol_bindings(place_id) { if let Some(def) = binding.binding.definition() { resolved.extend(resolve_definition( db, @@ -332,7 +332,7 @@ pub fn definitions_for_attribute<'db>( let use_def = index.use_def_map(function_scope_id); // Check declarations first - for decl in use_def.all_reachable_member_declarations(place_id) { + for decl in use_def.reachable_member_declarations(place_id) { if let Some(def) = decl.declaration.definition() { resolved.extend(resolve_definition( db, @@ -345,7 +345,7 @@ pub fn definitions_for_attribute<'db>( } // If no declarations found, check bindings - for binding in use_def.all_reachable_member_bindings(place_id) { + for binding in use_def.reachable_member_bindings(place_id) { if let Some(def) = binding.binding.definition() { resolved.extend(resolve_definition( db, @@ -1096,8 +1096,8 @@ mod resolve_definition { let mut definitions = IndexSet::new(); // Get all definitions (both bindings and declarations) for this place - let bindings = use_def_map.all_reachable_symbol_bindings(symbol_id); - let declarations = use_def_map.all_reachable_symbol_declarations(symbol_id); + let bindings = use_def_map.reachable_symbol_bindings(symbol_id); + let declarations = use_def_map.reachable_symbol_declarations(symbol_id); for binding in bindings { if let Some(def) = binding.binding.definition() { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 5c733454a0..94366f8cb3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8894,7 +8894,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // 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(db, use_def.all_reachable_bindings(place_id)).place + place_from_bindings(db, use_def.reachable_bindings(place_id)).place } else { assert!( self.deferred_state.in_string_annotation(), diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 5d6328a606..b80873992a 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -129,7 +129,7 @@ fn check_class_declaration<'db>( && PROHIBITED_NAMEDTUPLE_ATTRS.contains(&member.name.as_str()) && let Some(symbol_id) = place_table(db, class_scope).symbol_id(&member.name) && let Some(bad_definition) = use_def_map(db, class_scope) - .all_reachable_bindings(ScopedPlaceId::Symbol(symbol_id)) + .reachable_bindings(ScopedPlaceId::Symbol(symbol_id)) .filter_map(|binding| binding.binding.definition()) .find(|def| !matches!(def.kind(db), DefinitionKind::AnnotatedAssignment(_))) && let Some(builder) = context.report_lint( From 1dcb7f89f1631e69a1e085d2bca2b1f133258532 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 10 Dec 2025 11:20:24 -0500 Subject: [PATCH 24/36] [ty] Rename `all_members_of_scope` to `all_end_of_scope_members` This reflects more precisely its behavior based on how it uses the use-def map. --- crates/ty_python_semantic/src/semantic_model.rs | 6 +++--- crates/ty_python_semantic/src/types/list_members.rs | 6 +++--- crates/ty_python_semantic/src/types/overrides.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 4dc8a59bab..af887277d2 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -11,7 +11,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module}; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::semantic_index; -use crate::types::list_members::{Member, all_members, all_members_of_scope}; +use crate::types::list_members::{Member, all_end_of_scope_members, all_members}; use crate::types::{Type, binding_type, infer_scope_types}; use crate::{Db, resolve_real_shadowable_module}; @@ -76,7 +76,7 @@ impl<'db> SemanticModel<'db> { for (file_scope, _) in index.ancestor_scopes(file_scope) { for memberdef in - all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)) + all_end_of_scope_members(self.db, file_scope.to_scope_id(self.db, self.file)) { members.insert( memberdef.member.name, @@ -221,7 +221,7 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)).map( + all_end_of_scope_members(self.db, file_scope.to_scope_id(self.db, self.file)).map( |memberdef| Completion { name: memberdef.member.name, ty: Some(memberdef.member.ty), diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index eb47745b74..81ddac3d5b 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -26,7 +26,7 @@ use crate::{ }; /// Iterate over all declarations and bindings in the given scope. -pub(crate) fn all_members_of_scope<'db>( +pub(crate) fn all_end_of_scope_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { @@ -359,7 +359,7 @@ impl<'db> AllMembers<'db> { .map(|class| class.class_literal(db).0) { let parent_scope = parent.body_scope(db); - for memberdef in all_members_of_scope(db, parent_scope) { + for memberdef in all_end_of_scope_members(db, parent_scope) { let result = ty.member(db, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; @@ -407,7 +407,7 @@ impl<'db> AllMembers<'db> { // class member. This gets us the right type for each // member, e.g., `SomeClass.__delattr__` is not a bound // method, but `instance_of_SomeClass.__delattr__` is. - for memberdef in all_members_of_scope(db, class_body_scope) { + for memberdef in all_end_of_scope_members(db, class_body_scope) { let result = ty.member(db, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index b80873992a..c56581358c 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -25,7 +25,7 @@ use crate::{ report_overridden_final_method, }, function::{FunctionDecorators, FunctionType, KnownFunction}, - list_members::{Member, MemberWithDefinition, all_members_of_scope}, + list_members::{Member, MemberWithDefinition, all_end_of_scope_members}, }, }; @@ -54,7 +54,7 @@ pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLite let class_specialized = class.identity_specialization(db); let scope = class.body_scope(db); - let own_class_members: FxHashSet<_> = all_members_of_scope(db, scope).collect(); + let own_class_members: FxHashSet<_> = all_end_of_scope_members(db, scope).collect(); for member in own_class_members { check_class_declaration(context, configuration, class_specialized, scope, &member); From 8647844572e405c241a96dbc37e51cfa6b6b69a9 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 10 Dec 2025 11:17:00 -0500 Subject: [PATCH 25/36] [ty] Adjust scope completions to use all reachable symbols Fixes astral-sh/ty#1294 --- crates/ty_ide/src/completion.rs | 149 ++++++++++++++++++ .../src/semantic_index/use_def.rs | 24 +++ .../ty_python_semantic/src/semantic_model.rs | 6 +- .../src/types/list_members.rs | 57 ++++++- 4 files changed, 232 insertions(+), 4 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index b03b1930ca..ca2305df0c 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -6433,6 +6433,155 @@ collabc assert_snapshot!(snapshot, @"collections.abc"); } + #[test] + fn local_function_variable_with_return() { + let builder = completion_test_builder( + "\ +variable_global = 1 +def foo(): + variable_local = 1 + variable_ + return +", + ); + assert_snapshot!( + builder.skip_auto_import().build().snapshot(), + @r" + variable_global + variable_local + ", + ); + } + + #[test] + fn nested_scopes_with_return() { + let builder = completion_test_builder( + "\ +variable_1 = 1 +def fun1(): + variable_2 = 1 + def fun2(): + variable_3 = 1 + def fun3(): + variable_4 = 1 + variable_ + return + return + return +", + ); + assert_snapshot!( + builder.skip_auto_import().build().snapshot(), + @r" + variable_1 + variable_2 + variable_3 + variable_4 + ", + ); + } + + #[test] + fn multiple_declarations_global_scope1() { + let builder = completion_test_builder( + "\ +zqzqzq: int = 1 +zqzqzq: str = 'foo' +zqzq +", + ); + // The type for `zqzqzq` *should* be `str`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `int` binding + // isn't live at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_global_scope2() { + let builder = completion_test_builder( + "\ +zqzqzq: int = 1 +zqzq +zqzqzq: str = 'foo' +", + ); + // The type for `zqzqzq` *should* be `int`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `str` binding + // doesn't exist at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_function_scope1() { + let builder = completion_test_builder( + "\ +def foo(): + zqzqzq: int = 1 + zqzqzq: str = 'foo' + zqzq + return +", + ); + // The type for `zqzqzq` *should* be `str`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `int` binding + // isn't live at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_function_scope2() { + let builder = completion_test_builder( + "\ +def foo(): + zqzqzq: int = 1 + zqzq + zqzqzq: str = 'foo' + return +", + ); + // The type for `zqzqzq` *should* be `int`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `str` binding + // doesn't exist at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_function_parameter() { + let builder = completion_test_builder( + "\ +from pathlib import Path +def f(zqzqzq: str): + zqzqzq: Path = Path(zqzqzq) + zqzq + return +", + ); + // The type for `zqzqzq` *should* be `Path`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `str` and `Path` here even though the `str` binding + // isn't live at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: str | Path", + ); + } + /// A way to create a simple single-file (named `main.py`) completion test /// builder. /// diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index 5683fc736b..dbd26595fd 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -590,6 +590,30 @@ impl<'db> UseDefMap<'db> { .map(|symbol_id| (symbol_id, self.end_of_scope_symbol_bindings(symbol_id))) } + pub(crate) fn all_reachable_symbols<'map>( + &'map self, + ) -> impl Iterator< + Item = ( + ScopedSymbolId, + DeclarationsIterator<'map, 'db>, + BindingWithConstraintsIterator<'map, 'db>, + ), + > + 'map { + self.reachable_definitions_by_symbol.iter_enumerated().map( + |(symbol_id, reachable_definitions)| { + let declarations = self.declarations_iterator( + &reachable_definitions.declarations, + BoundnessAnalysis::AssumeBound, + ); + let bindings = self.bindings_iterator( + &reachable_definitions.bindings, + BoundnessAnalysis::AssumeBound, + ); + (symbol_id, declarations, bindings) + }, + ) + } + /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. pub(crate) fn can_implicitly_return_none(&self, db: &dyn crate::Db) -> bool { !self diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index af887277d2..54ca0ba74f 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -11,7 +11,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module}; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::semantic_index; -use crate::types::list_members::{Member, all_end_of_scope_members, all_members}; +use crate::types::list_members::{Member, all_members, all_reachable_members}; use crate::types::{Type, binding_type, infer_scope_types}; use crate::{Db, resolve_real_shadowable_module}; @@ -76,7 +76,7 @@ impl<'db> SemanticModel<'db> { for (file_scope, _) in index.ancestor_scopes(file_scope) { for memberdef in - all_end_of_scope_members(self.db, file_scope.to_scope_id(self.db, self.file)) + all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)) { members.insert( memberdef.member.name, @@ -221,7 +221,7 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_end_of_scope_members(self.db, file_scope.to_scope_id(self.db, self.file)).map( + all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)).map( |memberdef| Completion { name: memberdef.member.name, ty: Some(memberdef.member.ty), diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 81ddac3d5b..a93438a596 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -25,7 +25,8 @@ use crate::{ }, }; -/// Iterate over all declarations and bindings in the given scope. +/// Iterate over all declarations and bindings that exist at the end +/// of the given scope. pub(crate) fn all_end_of_scope_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, @@ -75,6 +76,60 @@ pub(crate) fn all_end_of_scope_members<'db>( )) } +/// Iterate over all declarations and bindings that are reachable anywhere +/// in the given scope. +pub(crate) fn all_reachable_members<'db>( + db: &'db dyn Db, + scope_id: ScopeId<'db>, +) -> impl Iterator> + 'db { + let use_def_map = use_def_map(db, scope_id); + let table = place_table(db, scope_id); + + use_def_map + .all_reachable_symbols() + .flat_map(move |(symbol_id, declarations, bindings)| { + let symbol = table.symbol(symbol_id); + + let declaration_place_result = place_from_declarations(db, declarations); + let declaration = + declaration_place_result + .first_declaration + .and_then(|first_reachable_definition| { + let ty = declaration_place_result + .ignore_conflicting_declarations() + .place + .ignore_possibly_undefined()?; + let member = Member { + name: symbol.name().clone(), + ty, + }; + Some(MemberWithDefinition { + member, + first_reachable_definition, + }) + }); + + let place_with_definition = place_from_bindings(db, bindings); + let binding = + place_with_definition + .first_definition + .and_then(|first_reachable_definition| { + let ty = place_with_definition.place.ignore_possibly_undefined()?; + let member = Member { + name: symbol.name().clone(), + ty, + }; + Some(MemberWithDefinition { + member, + first_reachable_definition, + }) + }); + + [declaration, binding] + }) + .flatten() +} + // `__init__`, `__repr__`, `__eq__`, `__ne__` and `__hash__` are always included via `object`, // so we don't need to list them here. const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[ From c9155d5e7233ee3ccc42c9846d5c7e525c06f40f Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 11 Dec 2025 14:36:16 +0100 Subject: [PATCH 26/36] [ty] Reduce size of ty-ide snapshots (#21915) --- crates/ty_ide/src/find_references.rs | 1082 ++++----------- crates/ty_ide/src/goto_declaration.rs | 1478 +++++++++------------ crates/ty_ide/src/goto_definition.rs | 930 ++++++------- crates/ty_ide/src/goto_type_definition.rs | 994 +++++++------- crates/ty_ide/src/lib.rs | 5 + 5 files changed, 1756 insertions(+), 2733 deletions(-) diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index e62608e555..274279c4e5 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -35,8 +35,6 @@ mod tests { use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; use insta::assert_snapshot; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span}; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; impl CursorTest { fn references(&self) -> String { @@ -52,20 +50,14 @@ mod tests { reference_results.sort_by_key(ReferenceTarget::file); - self.render_diagnostics(reference_results.into_iter().enumerate().map( - |(i, ref_item)| -> ReferenceResult { - ReferenceResult { - index: i, - file_range: FileRange::new(ref_item.file(), ref_item.range()), - } - }, - )) + self.render_diagnostics([ReferenceResult { + references: reference_results, + }]) } } struct ReferenceResult { - index: usize, - file_range: FileRange, + references: Vec, } impl IntoDiagnostic for ReferenceResult { @@ -73,11 +65,14 @@ mod tests { let mut main = Diagnostic::new( DiagnosticId::Lint(LintName::of("references")), Severity::Info, - format!("Reference {}", self.index + 1), + format!("Found {} references", self.references.len()), ); - main.annotate(Annotation::primary( - Span::from(self.file_range.file()).with_range(self.file_range.range()), - )); + + for reference in self.references { + main.annotate(Annotation::secondary( + Span::from(reference.file()).with_range(reference.range()), + )); + } main } @@ -97,55 +92,24 @@ result = calculate_sum(value=42) ", ); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 + assert_snapshot!(test.references(), @r" + info[references]: Found 5 references --> main.py:2:19 | 2 | def calculate_sum(value: int) -> int: - | ^^^^^ + | ----- 3 | doubled = value * 2 + | ----- 4 | result = value + doubled - | - - info[references]: Reference 2 - --> main.py:3:15 - | - 2 | def calculate_sum(value: int) -> int: - 3 | doubled = value * 2 - | ^^^^^ - 4 | result = value + doubled + | ----- 5 | return value - | - - info[references]: Reference 3 - --> main.py:4:14 - | - 2 | def calculate_sum(value: int) -> int: - 3 | doubled = value * 2 - 4 | result = value + doubled - | ^^^^^ - 5 | return value - | - - info[references]: Reference 4 - --> main.py:5:12 - | - 3 | doubled = value * 2 - 4 | result = value + doubled - 5 | return value - | ^^^^^ + | ----- 6 | - 7 | # Call with keyword argument - | - - info[references]: Reference 5 - --> main.py:8:24 - | 7 | # Call with keyword argument 8 | result = calculate_sum(value=42) - | ^^^^^ + | ----- | - "###); + "); } #[test] @@ -176,95 +140,36 @@ def outer_function(): ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:3:5 - | - 2 | def outer_function(): - 3 | counter = 0 - | ^^^^^^^ - 4 | - 5 | def increment(): - | - - info[references]: Reference 2 - --> main.py:6:18 - | - 5 | def increment(): - 6 | nonlocal counter - | ^^^^^^^ - 7 | counter += 1 - 8 | return counter - | - - info[references]: Reference 3 - --> main.py:7:9 - | - 5 | def increment(): - 6 | nonlocal counter - 7 | counter += 1 - | ^^^^^^^ - 8 | return counter - | - - info[references]: Reference 4 - --> main.py:8:16 + info[references]: Found 9 references + --> main.py:3:5 | + 2 | def outer_function(): + 3 | counter = 0 + | ------- + 4 | + 5 | def increment(): 6 | nonlocal counter + | ------- 7 | counter += 1 + | ------- 8 | return counter - | ^^^^^^^ + | ------- 9 | - 10 | def decrement(): - | - - info[references]: Reference 5 - --> main.py:11:18 - | 10 | def decrement(): 11 | nonlocal counter - | ^^^^^^^ + | ------- 12 | counter -= 1 + | ------- 13 | return counter - | - - info[references]: Reference 6 - --> main.py:12:9 - | - 10 | def decrement(): - 11 | nonlocal counter - 12 | counter -= 1 - | ^^^^^^^ - 13 | return counter - | - - info[references]: Reference 7 - --> main.py:13:16 - | - 11 | nonlocal counter - 12 | counter -= 1 - 13 | return counter - | ^^^^^^^ + | ------- 14 | - 15 | # Use counter in outer scope - | - - info[references]: Reference 8 - --> main.py:16:15 - | 15 | # Use counter in outer scope 16 | initial = counter - | ^^^^^^^ - 17 | increment() - 18 | decrement() - | - - info[references]: Reference 9 - --> main.py:19:13 - | + | ------- 17 | increment() 18 | decrement() 19 | final = counter - | ^^^^^^^ + | ------- 20 | 21 | return increment, decrement | @@ -296,94 +201,35 @@ final_value = global_counter ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:2:1 - | - 2 | global_counter = 0 - | ^^^^^^^^^^^^^^ - 3 | - 4 | def increment_global(): - | - - info[references]: Reference 2 - --> main.py:5:12 - | - 4 | def increment_global(): - 5 | global global_counter - | ^^^^^^^^^^^^^^ - 6 | global_counter += 1 - 7 | return global_counter - | - - info[references]: Reference 3 - --> main.py:6:5 - | - 4 | def increment_global(): - 5 | global global_counter - 6 | global_counter += 1 - | ^^^^^^^^^^^^^^ - 7 | return global_counter - | - - info[references]: Reference 4 - --> main.py:7:12 - | - 5 | global global_counter - 6 | global_counter += 1 - 7 | return global_counter - | ^^^^^^^^^^^^^^ - 8 | - 9 | def decrement_global(): - | - - info[references]: Reference 5 - --> main.py:10:12 + info[references]: Found 9 references + --> main.py:2:1 | + 2 | global_counter = 0 + | -------------- + 3 | + 4 | def increment_global(): + 5 | global global_counter + | -------------- + 6 | global_counter += 1 + | -------------- + 7 | return global_counter + | -------------- + 8 | 9 | def decrement_global(): 10 | global global_counter - | ^^^^^^^^^^^^^^ + | -------------- 11 | global_counter -= 1 + | -------------- 12 | return global_counter - | - - info[references]: Reference 6 - --> main.py:11:5 - | - 9 | def decrement_global(): - 10 | global global_counter - 11 | global_counter -= 1 - | ^^^^^^^^^^^^^^ - 12 | return global_counter - | - - info[references]: Reference 7 - --> main.py:12:12 - | - 10 | global global_counter - 11 | global_counter -= 1 - 12 | return global_counter - | ^^^^^^^^^^^^^^ + | -------------- 13 | - 14 | # Use global_counter at module level - | - - info[references]: Reference 8 - --> main.py:15:17 - | 14 | # Use global_counter at module level 15 | initial_value = global_counter - | ^^^^^^^^^^^^^^ - 16 | increment_global() - 17 | decrement_global() - | - - info[references]: Reference 9 - --> main.py:18:15 - | + | -------------- 16 | increment_global() 17 | decrement_global() 18 | final_value = global_counter - | ^^^^^^^^^^^^^^ + | -------------- | "); } @@ -406,45 +252,23 @@ except ValueError as err: ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:4:29 - | - 2 | try: - 3 | x = 1 / 0 - 4 | except ZeroDivisionError as err: - | ^^^ - 5 | print(f'Error: {err}') - 6 | return err - | - - info[references]: Reference 2 - --> main.py:5:21 - | - 3 | x = 1 / 0 - 4 | except ZeroDivisionError as err: - 5 | print(f'Error: {err}') - | ^^^ - 6 | return err - | - - info[references]: Reference 3 - --> main.py:6:12 - | - 4 | except ZeroDivisionError as err: - 5 | print(f'Error: {err}') - 6 | return err - | ^^^ - 7 | - 8 | try: - | - - info[references]: Reference 4 - --> main.py:11:31 + info[references]: Found 4 references + --> main.py:4:29 | + 2 | try: + 3 | x = 1 / 0 + 4 | except ZeroDivisionError as err: + | --- + 5 | print(f'Error: {err}') + | --- + 6 | return err + | --- + 7 | + 8 | try: 9 | y = 2 / 0 10 | except ValueError as err: 11 | print(f'Different error: {err}') - | ^^^ + | --- | "); } @@ -462,39 +286,21 @@ match x: ", ); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 + assert_snapshot!(test.references(), @r" + info[references]: Found 3 references --> main.py:3:20 | 2 | match x: 3 | case [a, b] as pattern: - | ^^^^^^^ + | ------- 4 | print(f'Matched: {pattern}') + | ------- 5 | return pattern - | - - info[references]: Reference 2 - --> main.py:4:27 - | - 2 | match x: - 3 | case [a, b] as pattern: - 4 | print(f'Matched: {pattern}') - | ^^^^^^^ - 5 | return pattern - 6 | case _: - | - - info[references]: Reference 3 - --> main.py:5:16 - | - 3 | case [a, b] as pattern: - 4 | print(f'Matched: {pattern}') - 5 | return pattern - | ^^^^^^^ + | ------- 6 | case _: 7 | pass | - "###); + "); } #[test] @@ -509,47 +315,21 @@ match data: ", ); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 + assert_snapshot!(test.references(), @r" + info[references]: Found 4 references --> main.py:3:29 | 2 | match data: 3 | case {'a': a, 'b': b, **rest}: - | ^^^^ + | ---- 4 | print(f'Rest data: {rest}') + | ---- 5 | process(rest) - | - - info[references]: Reference 2 - --> main.py:4:29 - | - 2 | match data: - 3 | case {'a': a, 'b': b, **rest}: - 4 | print(f'Rest data: {rest}') - | ^^^^ - 5 | process(rest) + | ---- 6 | return rest + | ---- | - - info[references]: Reference 3 - --> main.py:5:17 - | - 3 | case {'a': a, 'b': b, **rest}: - 4 | print(f'Rest data: {rest}') - 5 | process(rest) - | ^^^^ - 6 | return rest - | - - info[references]: Reference 4 - --> main.py:6:16 - | - 4 | print(f'Rest data: {rest}') - 5 | process(rest) - 6 | return rest - | ^^^^ - | - "###); + "); } #[test] @@ -573,60 +353,30 @@ value = my_function ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return 42 - | - - info[references]: Reference 2 - --> main.py:6:11 - | - 5 | # Call the function multiple times - 6 | result1 = my_function() - | ^^^^^^^^^^^ - 7 | result2 = my_function() - | - - info[references]: Reference 3 - --> main.py:7:11 - | - 5 | # Call the function multiple times - 6 | result1 = my_function() - 7 | result2 = my_function() - | ^^^^^^^^^^^ - 8 | - 9 | # Function passed as an argument - | - - info[references]: Reference 4 - --> main.py:10:12 + info[references]: Found 6 references + --> main.py:2:5 | + 2 | def my_function(): + | ----------- + 3 | return 42 + | + ::: main.py:6:11 + | + 5 | # Call the function multiple times + 6 | result1 = my_function() + | ----------- + 7 | result2 = my_function() + | ----------- + 8 | 9 | # Function passed as an argument 10 | callback = my_function - | ^^^^^^^^^^^ + | ----------- 11 | - 12 | # Function used in different contexts - | - - info[references]: Reference 5 - --> main.py:13:7 - | 12 | # Function used in different contexts 13 | print(my_function()) - | ^^^^^^^^^^^ + | ----------- 14 | value = my_function - | - - info[references]: Reference 6 - --> main.py:14:9 - | - 12 | # Function used in different contexts - 13 | print(my_function()) - 14 | value = my_function - | ^^^^^^^^^^^ + | ----------- | "); } @@ -653,59 +403,32 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:2:7 - | - 2 | class MyClass: - | ^^^^^^^ - 3 | def __init__(self): - 4 | pass - | - - info[references]: Reference 2 - --> main.py:7:8 - | - 6 | # Create instances - 7 | obj1 = MyClass() - | ^^^^^^^ - 8 | obj2 = MyClass() - | - - info[references]: Reference 3 - --> main.py:8:8 + info[references]: Found 6 references + --> main.py:2:7 + | + 2 | class MyClass: + | ------- + 3 | def __init__(self): + 4 | pass + | + ::: main.py:7:8 | 6 | # Create instances 7 | obj1 = MyClass() + | ------- 8 | obj2 = MyClass() - | ^^^^^^^ + | ------- 9 | - 10 | # Use in type annotations - | - - info[references]: Reference 4 - --> main.py:11:23 - | 10 | # Use in type annotations 11 | def process(instance: MyClass) -> MyClass: - | ^^^^^^^ + | ------- ------- 12 | return instance | - - info[references]: Reference 5 - --> main.py:11:35 - | - 10 | # Use in type annotations - 11 | def process(instance: MyClass) -> MyClass: - | ^^^^^^^ - 12 | return instance - | - - info[references]: Reference 6 - --> main.py:15:7 + ::: main.py:15:7 | 14 | # Reference the class itself 15 | cls = MyClass - | ^^^^^^^ + | ------- | "); } @@ -722,22 +445,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:5 | 2 | a: "MyClass" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -755,22 +470,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:12 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -802,22 +509,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:12 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -863,22 +562,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:5 | 2 | a: "MyClass | No" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "MyClass | No" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -907,18 +598,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:1 | 2 | ab: "ab" - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:6 - | - 2 | ab: "ab" - | ^^ + | -- -- | "#); } @@ -946,23 +630,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -979,23 +655,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1012,23 +680,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", *ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1045,23 +705,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", *ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1078,23 +730,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ("a" | "b") as ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1111,23 +755,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ("a" | "b") as ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1150,23 +786,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | -- 11 | x = ab - | - - info[references]: Reference 2 - --> main.py:11:17 - | - 9 | match event: - 10 | case Click(x, button=ab): - 11 | x = ab - | ^^ + | -- | "); } @@ -1189,23 +817,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | -- 11 | x = ab - | - - info[references]: Reference 2 - --> main.py:11:17 - | - 9 | match event: - 10 | case Click(x, button=ab): - 11 | x = ab - | ^^ + | -- | "); } @@ -1228,33 +848,23 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 - --> main.py:2:7 - | - 2 | class Click: - | ^^^^^ - 3 | __match_args__ = ("position", "button") - 4 | def __init__(self, pos, btn): - | - - info[references]: Reference 2 - --> main.py:8:20 + info[references]: Found 3 references + --> main.py:2:7 + | + 2 | class Click: + | ----- + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | + ::: main.py:8:20 | 6 | self.button: str = btn 7 | 8 | def my_func(event: Click): - | ^^^^^ + | ----- 9 | match event: 10 | case Click(x, button=ab): - | - - info[references]: Reference 3 - --> main.py:10:14 - | - 8 | def my_func(event: Click): - 9 | match event: - 10 | case Click(x, button=ab): - | ^^^^^ + | ----- 11 | x = ab | "#); @@ -1289,25 +899,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:37 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:46 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- -- -- | "); } @@ -1321,25 +917,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:37 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:46 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- -- -- | "); } @@ -1354,28 +936,12 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:3:43 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:3:53 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | -- -- -- | "); } @@ -1390,28 +956,12 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:3:43 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:3:53 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | -- -- -- | "); } @@ -1425,25 +975,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:38 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:50 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | -- -- -- | "); } @@ -1457,25 +993,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:38 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:50 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | -- -- -- | "); } @@ -1515,57 +1037,35 @@ class DataProcessor: .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> utils.py:2:5 - | - 2 | def func(x): - | ^^^^ - 3 | return x * 2 - | - - info[references]: Reference 2 - --> module.py:2:19 - | - 2 | from utils import func - | ^^^^ - 3 | - 4 | def process_data(data): - | - - info[references]: Reference 3 - --> module.py:5:12 - | - 4 | def process_data(data): - 5 | return func(data) - | ^^^^ - | - - info[references]: Reference 4 + info[references]: Found 6 references --> app.py:2:19 | 2 | from utils import func - | ^^^^ + | ---- 3 | - 4 | class DataProcessor: - | - - info[references]: Reference 5 - --> app.py:6:27 - | 4 | class DataProcessor: 5 | def __init__(self): 6 | self.multiplier = func - | ^^^^ + | ---- 7 | - 8 | def process(self, value): - | - - info[references]: Reference 6 - --> app.py:9:16 - | 8 | def process(self, value): 9 | return func(value) - | ^^^^ + | ---- + | + ::: module.py:2:19 + | + 2 | from utils import func + | ---- + 3 | + 4 | def process_data(data): + 5 | return func(data) + | ---- + | + ::: utils.py:2:5 + | + 2 | def func(x): + | ---- + 3 | return x * 2 | "); } @@ -1598,52 +1098,27 @@ def process_model(): .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> models.py:3:5 - | - 2 | class MyModel: - 3 | attr = 42 - | ^^^^ - 4 | - 5 | def get_attribute(self): - | - - info[references]: Reference 2 - --> models.py:6:24 - | - 5 | def get_attribute(self): - 6 | return MyModel.attr - | ^^^^ - | - - info[references]: Reference 3 + info[references]: Found 5 references --> main.py:6:19 | 4 | def process_model(): 5 | model = MyModel() 6 | value = model.attr - | ^^^^ + | ---- 7 | model.attr = 100 + | ---- 8 | return model.attr + | ---- | - - info[references]: Reference 4 - --> main.py:7:11 + ::: models.py:3:5 | - 5 | model = MyModel() - 6 | value = model.attr - 7 | model.attr = 100 - | ^^^^ - 8 | return model.attr - | - - info[references]: Reference 5 - --> main.py:8:18 - | - 6 | value = model.attr - 7 | model.attr = 100 - 8 | return model.attr - | ^^^^ + 2 | class MyModel: + 3 | attr = 42 + | ---- + 4 | + 5 | def get_attribute(self): + 6 | return MyModel.attr + | ---- | "); } @@ -1673,22 +1148,14 @@ func_alias() // When finding references to the alias, we should NOT find references // to the original function in the original module assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> importer.py:2:30 | 2 | from original import func as func_alias - | ^^^^^^^^^^ + | ---------- 3 | 4 | func_alias() - | - - info[references]: Reference 2 - --> importer.py:4:1 - | - 2 | from original import func as func_alias - 3 | - 4 | func_alias() - | ^^^^^^^^^^ + | ---------- | "); } @@ -1721,42 +1188,23 @@ func_alias() ) .build(); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 - --> path.pyi:2:7 - | - 2 | class Path: - | ^^^^ - 3 | def __init__(self, path: str): ... - | - - info[references]: Reference 2 + assert_snapshot!(test.references(), @r#" + info[references]: Found 4 references --> importer.py:2:18 | 2 | from path import Path - | ^^^^ + | ---- 3 | 4 | a: Path = Path("test") + | ---- ---- | - - info[references]: Reference 3 - --> importer.py:4:4 + ::: path.pyi:2:7 | - 2 | from path import Path - 3 | - 4 | a: Path = Path("test") - | ^^^^ + 2 | class Path: + | ---- + 3 | def __init__(self, path: str): ... | - - info[references]: Reference 4 - --> importer.py:4:11 - | - 2 | from path import Path - 3 | - 4 | a: Path = Path("test") - | ^^^^ - | - "###); + "#); } #[test] @@ -1775,23 +1223,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:3:20 | 2 | import warnings 3 | import warnings as abc - | ^^^ + | --- 4 | 5 | x = abc - | - - info[references]: Reference 2 - --> main.py:5:5 - | - 3 | import warnings as abc - 4 | - 5 | x = abc - | ^^^ + | --- 6 | y = warnings | "); @@ -1813,23 +1253,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:3:20 | 2 | import warnings 3 | import warnings as abc - | ^^^ + | --- 4 | 5 | x = abc - | - - info[references]: Reference 2 - --> main.py:5:5 - | - 3 | import warnings as abc - 4 | - 5 | x = abc - | ^^^ + | --- 6 | y = warnings | "); @@ -1851,21 +1283,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:36 | 2 | from warnings import deprecated as xyz - | ^^^ - 3 | from warnings import deprecated - | - - info[references]: Reference 2 - --> main.py:5:5 - | + | --- 3 | from warnings import deprecated 4 | 5 | y = xyz - | ^^^ + | --- 6 | z = deprecated | "); @@ -1887,21 +1313,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:36 | 2 | from warnings import deprecated as xyz - | ^^^ - 3 | from warnings import deprecated - | - - info[references]: Reference 2 - --> main.py:5:5 - | + | --- 3 | from warnings import deprecated 4 | 5 | y = xyz - | ^^^ + | --- 6 | z = deprecated | "); @@ -1929,13 +1349,13 @@ func_alias() // TODO(submodule-imports): this should light up both instances of `subpkg` assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 1 references --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = subpkg - | ^^^^^^ + | ------ | "); } @@ -2055,29 +1475,19 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> mypackage/__init__.py:2:21 | 2 | from .subpkg import subpkg - | ^^^^^^ + | ------ 3 | 4 | x = subpkg + | ------ | - - info[references]: Reference 2 - --> mypackage/__init__.py:4:5 - | - 2 | from .subpkg import subpkg - 3 | - 4 | x = subpkg - | ^^^^^^ - | - - info[references]: Reference 3 - --> mypackage/subpkg/__init__.py:2:1 + ::: mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 - | ^^^^^^ + | ------ | "); } @@ -2103,13 +1513,13 @@ func_alias() // TODO: this should also highlight the RHS subpkg in the import assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 1 references --> mypackage/__init__.py:4:5 | 2 | from .subpkg import subpkg 3 | 4 | x = subpkg - | ^^^^^^ + | ------ | "); } @@ -2131,33 +1541,17 @@ func_alias() .build(); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:1 | 2 | a: str = "test" - | ^ + | - 3 | 4 | a: int = 10 - | - - info[references]: Reference 2 - --> main.py:4:1 - | - 2 | a: str = "test" - 3 | - 4 | a: int = 10 - | ^ + | - 5 | 6 | print(a) - | - - info[references]: Reference 3 - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ + | - | "#); } diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 8425654e19..114d43e3b8 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -31,15 +31,9 @@ pub fn goto_declaration( #[cfg(test)] mod tests { - use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; - use crate::{NavigationTarget, goto_declaration}; + use crate::goto_declaration; + use crate::tests::{CursorTest, cursor_test}; use insta::assert_snapshot; - use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, - SubDiagnosticSeverity, - }; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; #[test] fn goto_declaration_function_call_to_definition() { @@ -53,20 +47,20 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:5 - | - 2 | def my_function(x, y): - | ^^^^^^^^^^^ - 3 | return x + y - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:10 | 3 | return x + y 4 | 5 | result = my_function(1, 2) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:5 + | + 2 | def my_function(x, y): + | ----------- + 3 | return x + y | "); } @@ -81,19 +75,19 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | x = 42 - | ^ - 3 | y = x - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:5 | 2 | x = 42 3 | y = x - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | x = 42 + | - + 3 | y = x | "); } @@ -111,39 +105,23 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:6:12 + | + 4 | pass + 5 | + 6 | instance = MyClass() + | ^^^^^^^ Clicking here + | + info: Found 2 declarations --> main.py:2:7 | 2 | class MyClass: - | ^^^^^^^ + | ------- 3 | def __init__(self): + | -------- 4 | pass | - info: Source - --> main.py:6:12 - | - 4 | pass - 5 | - 6 | instance = MyClass() - | ^^^^^^^ - | - - info[goto-declaration]: Declaration - --> main.py:3:9 - | - 2 | class MyClass: - 3 | def __init__(self): - | ^^^^^^^^ - 4 | pass - | - info: Source - --> main.py:6:12 - | - 4 | pass - 5 | - 6 | instance = MyClass() - | ^^^^^^^ - | "); } @@ -157,19 +135,19 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:9 - | - 2 | def foo(param): - | ^^^^^ - 3 | return param * 2 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:12 | 2 | def foo(param): 3 | return param * 2 - | ^^^^^ + | ^^^^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:9 + | + 2 | def foo(param): + | ----- + 3 | return param * 2 | "); } @@ -185,20 +163,20 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:18 - | - 2 | def generic_func[T](value: T) -> T: - | ^ - 3 | v: T = value - 4 | return v - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:8 | 2 | def generic_func[T](value: T) -> T: 3 | v: T = value - | ^ + | ^ Clicking here + 4 | return v + | + info: Found 1 declaration + --> main.py:2:18 + | + 2 | def generic_func[T](value: T) -> T: + | - + 3 | v: T = value 4 | return v | "); @@ -215,20 +193,20 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:20 - | - 2 | class GenericClass[T]: - | ^ - 3 | def __init__(self, value: T): - 4 | self.value = value - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:31 | 2 | class GenericClass[T]: 3 | def __init__(self, value: T): - | ^ + | ^ Clicking here + 4 | self.value = value + | + info: Found 1 declaration + --> main.py:2:20 + | + 2 | class GenericClass[T]: + | - + 3 | def __init__(self, value: T): 4 | self.value = value | "); @@ -247,23 +225,23 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | x = "outer" - | ^ - 3 | def outer_func(): - 4 | def inner_func(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:16 | 3 | def outer_func(): 4 | def inner_func(): 5 | return x # Should find outer x - | ^ + | ^ Clicking here 6 | return inner_func | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | x = "outer" + | - + 3 | def outer_func(): + 4 | def inner_func(): + | "#); } @@ -307,20 +285,20 @@ variable = 42 .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mymodule.py:1:1 - | - 1 | - | ^ - 2 | def function(): - 3 | return "hello from mymodule" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | import mymodule 3 | print(mymodule.function()) - | ^^^^^^^^ + | ^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> mymodule.py:1:1 + | + 1 | + | - + 2 | def function(): + 3 | return "hello from mymodule" | "#); } @@ -348,19 +326,19 @@ def other_function(): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mymodule.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | from mymodule import my_function 3 | print(my_function()) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ----------- + 3 | return "hello" | "#); } @@ -390,22 +368,22 @@ FOO = 0 .build(); // Should find the submodule file itself - assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mymodule/submodule.py:1:1 - | - 1 | - | ^ - 2 | FOO = 0 - | - info: Source + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | import mymodule.submodule as sub 3 | print(sub.helper()) - | ^^^ + | ^^^ Clicking here | - "#); + info: Found 1 declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | - + 2 | FOO = 0 + | + "); } #[test] @@ -429,19 +407,19 @@ def func(arg): // Should resolve to the actual function definition, not the import statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> utils.py:2:5 - | - 2 | def func(arg): - | ^^^^ - 3 | return f"Processed: {arg}" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | from utils import func as h 3 | print(h("test")) - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> utils.py:2:5 + | + 2 | def func(arg): + | ---- + 3 | return f"Processed: {arg}" | "#); } @@ -473,19 +451,19 @@ def shared_function(): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> original.py:2:5 - | - 2 | def shared_function(): - | ^^^^^^^^^^^^^^^ - 3 | return "from original" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | from intermediate import shared_function 3 | print(shared_function()) - | ^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> original.py:2:5 + | + 2 | def shared_function(): + | --------------- + 3 | return "from original" | "#); } @@ -515,20 +493,20 @@ def multiply_numbers(a, b): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> math_utils.py:2:5 - | - 2 | def add_numbers(a, b): - | ^^^^^^^^^^^ - 3 | """Add two numbers together.""" - 4 | return a + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:10 | 2 | from math_utils import * 3 | result = add_numbers(5, 3) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> math_utils.py:2:5 + | + 2 | def add_numbers(a, b): + | ----------- + 3 | """Add two numbers together.""" + 4 | return a + b | "#); } @@ -565,20 +543,20 @@ def another_helper(): // Should resolve the relative import to find the actual function definition assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> package/utils.py:2:5 - | - 2 | def helper_function(arg): - | ^^^^^^^^^^^^^^^ - 3 | """A helper function in utils module.""" - 4 | return f"Processed: {arg}" - | - info: Source + info[goto-declaration]: Go to declaration --> package/main.py:3:10 | 2 | from .utils import helper_function 3 | result = helper_function("test") - | ^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> package/utils.py:2:5 + | + 2 | def helper_function(arg): + | --------------- + 3 | """A helper function in utils module.""" + 4 | return f"Processed: {arg}" | "#); } @@ -614,20 +592,20 @@ def another_helper(): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> package/utils.py:2:5 - | - 2 | def helper_function(arg): - | ^^^^^^^^^^^^^^^ - 3 | """A helper function in utils module.""" - 4 | return f"Processed: {arg}" - | - info: Source + info[goto-declaration]: Go to declaration --> package/main.py:3:10 | 2 | from .utils import * 3 | result = helper_function("test") - | ^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> package/utils.py:2:5 + | + 2 | def helper_function(arg): + | --------------- + 3 | """A helper function in utils module.""" + 4 | return f"Processed: {arg}" | "#); } @@ -657,20 +635,20 @@ FOO = 0 .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule/submodule.py:1:1 - | - 1 | - | ^ - 2 | FOO = 0 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:30 | 2 | import mymodule.submodule as sub - | ^^^ + | ^^^ Clicking here 3 | print(sub.helper()) | + info: Found 1 declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | - + 2 | FOO = 0 + | "); } @@ -699,20 +677,20 @@ FOO = 0 .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule/submodule.py:1:1 - | - 1 | - | ^ - 2 | FOO = 0 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:17 | 2 | import mymodule.submodule as sub - | ^^^^^^^^^ + | ^^^^^^^^^ Clicking here 3 | print(sub.helper()) | + info: Found 1 declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | - + 2 | FOO = 0 + | "); } @@ -745,20 +723,20 @@ def another_helper(path): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mypackage/utils.py:2:5 - | - 2 | def helper(a, b): - | ^^^^^^ - 3 | return a + "/" + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:29 | 2 | from mypackage.utils import helper as h - | ^^^^^^ + | ^^^^^^ Clicking here 3 | result = h("/a", "/b") | + info: Found 1 declaration + --> mypackage/utils.py:2:5 + | + 2 | def helper(a, b): + | ------ + 3 | return a + "/" + b + | "#); } @@ -791,20 +769,20 @@ def another_helper(path): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mypackage/utils.py:2:5 - | - 2 | def helper(a, b): - | ^^^^^^ - 3 | return a + "/" + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:39 | 2 | from mypackage.utils import helper as h - | ^ + | ^ Clicking here 3 | result = h("/a", "/b") | + info: Found 1 declaration + --> mypackage/utils.py:2:5 + | + 2 | def helper(a, b): + | ------ + 3 | return a + "/" + b + | "#); } @@ -837,21 +815,21 @@ def another_helper(path): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mypackage/utils.py:1:1 - | - 1 | - | ^ - 2 | def helper(a, b): - 3 | return a + "/" + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:16 | 2 | from mypackage.utils import helper as h - | ^^^^^ + | ^^^^^ Clicking here 3 | result = h("/a", "/b") | + info: Found 1 declaration + --> mypackage/utils.py:1:1 + | + 1 | + | - + 2 | def helper(a, b): + 3 | return a + "/" + b + | "#); } @@ -869,23 +847,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:7:7 + | + 6 | c = C() + 7 | y = c.x + | ^ Clicking here + | + info: Found 1 declaration --> main.py:4:9 | 2 | class C: 3 | def __init__(self): 4 | self.x: int = 1 - | ^^^^^^ + | ------ 5 | 6 | c = C() | - info: Source - --> main.py:7:7 - | - 6 | c = C() - 7 | y = c.x - | ^ - | "); } @@ -901,23 +879,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:5 + | + 2 | a: "MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:5 - | - 2 | a: "MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -933,23 +911,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:12 + | + 2 | a: "None | MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "None | MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:12 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -979,23 +957,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:12 + | + 2 | a: "None | MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "None | MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:12 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -1039,23 +1017,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:5 + | + 2 | a: "MyClass | No" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "MyClass | No" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:5 - | - 2 | a: "MyClass | No" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -1082,17 +1060,17 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | ab: "ab" - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:6 | 2 | ab: "ab" - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | ab: "ab" + | -- | "#); } @@ -1126,23 +1104,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:11:9 + | + 10 | d = D() + 11 | y = d.y.x + | ^ Clicking here + | + info: Found 1 declaration --> main.py:4:9 | 2 | class C: 3 | def __init__(self): 4 | self.x: int = 1 - | ^^^^^^ + | ------ 5 | 6 | class D: | - info: Source - --> main.py:11:9 - | - 10 | d = D() - 11 | y = d.y.x - | ^ - | "); } @@ -1160,23 +1138,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:7:7 + | + 6 | c = C() + 7 | y = c.x + | ^ Clicking here + | + info: Found 1 declaration --> main.py:4:9 | 2 | class C: 3 | def __init__(self): 4 | self.x = 1 - | ^^^^^^ + | ------ 5 | 6 | c = C() | - info: Source - --> main.py:7:7 - | - 6 | c = C() - 7 | y = c.x - | ^ - | "); } @@ -1194,20 +1172,20 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:9 - | - 2 | class C: - 3 | def foo(self): - | ^^^ - 4 | return 42 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:7:9 | 6 | c = C() 7 | res = c.foo() - | ^^^ + | ^^^ Clicking here + | + info: Found 1 declaration + --> main.py:3:9 + | + 2 | class C: + 3 | def foo(self): + | --- + 4 | return 42 | "); } @@ -1244,7 +1222,7 @@ x: int = 42 "Should find the int class definition" ); assert!( - result.contains("info[goto-declaration]: Declaration"), + result.contains("info[goto-declaration]: Go to declaration"), "Should be a goto-declaration result" ); } @@ -1267,25 +1245,25 @@ def outer(): // Should find the variable declaration in the outer scope, not the nonlocal statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | def outer(): - 3 | x = "outer_value" - | ^ - 4 | - 5 | def inner(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:8:16 | 6 | nonlocal x 7 | x = "modified" 8 | return x # Should find the nonlocal x declaration in outer scope - | ^ + | ^ Clicking here 9 | 10 | return inner | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | def outer(): + 3 | x = "outer_value" + | - + 4 | + 5 | def inner(): + | "#); } @@ -1307,24 +1285,24 @@ def outer(): // Should find the variable declaration in the outer scope, not the nonlocal statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | def outer(): - 3 | xy = "outer_value" - | ^^ - 4 | - 5 | def inner(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:6:18 | 5 | def inner(): 6 | nonlocal xy - | ^^ + | ^^ Clicking here 7 | xy = "modified" 8 | return x # Should find the nonlocal x declaration in outer scope | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | def outer(): + 3 | xy = "outer_value" + | -- + 4 | + 5 | def inner(): + | "#); } @@ -1343,21 +1321,21 @@ def function(): // Should find the global variable declaration, not the global statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | global_var = "global_value" - | ^^^^^^^^^^ - 3 | - 4 | def function(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:7:12 | 5 | global global_var 6 | global_var = "modified" 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" + | ---------- + 3 | + 4 | def function(): | "#); } @@ -1377,23 +1355,23 @@ def function(): // Should find the global variable declaration, not the global statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | global_var = "global_value" - | ^^^^^^^^^^ - 3 | - 4 | def function(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:12 | 4 | def function(): 5 | global global_var - | ^^^^^^^^^^ + | ^^^^^^^^^^ Clicking here 6 | global_var = "modified" 7 | return global_var # Should find the global variable declaration | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | global_var = "global_value" + | ---------- + 3 | + 4 | def function(): + | "#); } @@ -1413,21 +1391,21 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | class A: - 3 | x = 10 - | ^ - 4 | - 5 | class B(A): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:9:7 | 8 | b = B() 9 | y = b.x - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | class A: + 3 | x = 10 + | - + 4 | + 5 | class B(A): | "); } @@ -1444,22 +1422,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | ^^ Clicking here 5 | x = ab | - info: Source + info: Found 1 declaration --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | -- 5 | x = ab | "#); @@ -1477,22 +1455,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:4:22 - | - 2 | def my_func(command: str): - 3 | match command.split(): - 4 | case ["get", ab]: - | ^^ - 5 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:17 | 3 | match command.split(): 4 | case ["get", ab]: 5 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | -- + 5 | x = ab | "#); } @@ -1509,22 +1487,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | ^^ Clicking here 5 | x = ab | - info: Source + info: Found 1 declaration --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | -- 5 | x = ab | "#); @@ -1542,22 +1520,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:4:23 - | - 2 | def my_func(command: str): - 3 | match command.split(): - 4 | case ["get", *ab]: - | ^^ - 5 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:17 | 3 | match command.split(): 4 | case ["get", *ab]: 5 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | -- + 5 | x = ab | "#); } @@ -1574,22 +1552,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | ^^ Clicking here 5 | x = ab | - info: Source + info: Found 1 declaration --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | -- 5 | x = ab | "#); @@ -1607,22 +1585,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:4:37 - | - 2 | def my_func(command: str): - 3 | match command.split(): - 4 | case ["get", ("a" | "b") as ab]: - | ^^ - 5 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:17 | 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: 5 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:4:37 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + | -- + 5 | x = ab | "#); } @@ -1645,22 +1623,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | ^^ Clicking here 11 | x = ab | - info: Source + info: Found 1 declaration --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | -- 11 | x = ab | "); @@ -1684,22 +1662,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:10:30 - | - 8 | def my_func(event: Click): - 9 | match event: - 10 | case Click(x, button=ab): - | ^^ - 11 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:11:17 | 9 | match event: 10 | case Click(x, button=ab): 11 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | -- + 11 | x = ab | "); } @@ -1722,23 +1700,23 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:7 - | - 2 | class Click: - | ^^^^^ - 3 | __match_args__ = ("position", "button") - 4 | def __init__(self, pos, btn): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:10:14 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^^^^ + | ^^^^^ Clicking here 11 | x = ab | + info: Found 1 declaration + --> main.py:2:7 + | + 2 | class Click: + | ----- + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | "#); } @@ -1771,17 +1749,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- | "); } @@ -1795,17 +1773,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:13 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:37 | 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]] + | -- | "); } @@ -1820,19 +1798,19 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 declaration --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | -- | "); } @@ -1847,19 +1825,19 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:15 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:43 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | -- | "); } @@ -1873,17 +1851,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | -- | "); } @@ -1897,17 +1875,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:14 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:38 | 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]] + | -- | "); } @@ -1930,21 +1908,21 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:7:9 - | - 6 | @property - 7 | def value(self): - | ^^^^^ - 8 | return self._value - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:11:3 | 10 | c = C() 11 | c.value = 42 - | ^^^^^ + | ^^^^^ Clicking here | + info: Found 1 declaration + --> main.py:7:9 + | + 6 | @property + 7 | def value(self): + | ----- + 8 | return self._value + | "); } @@ -1982,7 +1960,7 @@ def function(): "Should find the __doc__ attribute definition" ); assert!( - result.contains("info[goto-declaration]: Declaration"), + result.contains("info[goto-declaration]: Go to declaration"), "Should be a goto-declaration result" ); } @@ -2003,23 +1981,23 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:9:9 + | + 8 | def use_drawable(obj: Drawable): + 9 | obj.name + | ^^^^ Clicking here + | + info: Found 1 declaration --> main.py:6:5 | 4 | class Drawable(Protocol): 5 | def draw(self) -> None: ... 6 | name: str - | ^^^^ + | ---- 7 | 8 | def use_drawable(obj: Drawable): | - info: Source - --> main.py:9:9 - | - 8 | def use_drawable(obj: Drawable): - 9 | obj.name - | ^^^^ - | "); } @@ -2037,24 +2015,24 @@ class MyClass: // Should find the ClassType defined in the class body, not fail to resolve assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | class MyClass: - 3 | ClassType = int - | ^^^^^^^^^ - 4 | - 5 | def generic_method[T](self, value: ClassType) -> T: - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:40 | 3 | ClassType = int 4 | 5 | def generic_method[T](self, value: ClassType) -> T: - | ^^^^^^^^^ + | ^^^^^^^^^ Clicking here 6 | return value | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | class MyClass: + 3 | ClassType = int + | --------- + 4 | + 5 | def generic_method[T](self, value: ClassType) -> T: + | "); } @@ -2070,20 +2048,20 @@ class MyClass: ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:20 - | - 2 | def my_function(x, y, z=10): - | ^ - 3 | return x + y + z - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:25 | 3 | return x + y + z 4 | 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): + | - + 3 | return x + y + z | "); } @@ -2110,39 +2088,26 @@ class MyClass: // Should navigate to the parameter in both matching overloads assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:5:24 - | - 4 | @overload - 5 | def process(data: str, format: str) -> str: ... - | ^^^^^^ - 6 | - 7 | @overload - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:14:27 | 13 | # Call the overloaded function 14 | result = process("hello", format="json") - | ^^^^^^ + | ^^^^^^ Clicking here | - - info[goto-declaration]: Declaration - --> main.py:8:24 + info: Found 2 declarations + --> main.py:5:24 | + 4 | @overload + 5 | def process(data: str, format: str) -> str: ... + | ------ + 6 | 7 | @overload 8 | def process(data: int, format: int) -> int: ... - | ^^^^^^ + | ------ 9 | 10 | def process(data, format): | - info: Source - --> main.py:14:27 - | - 13 | # Call the overloaded function - 14 | result = process("hello", format="json") - | ^^^^^^ - | "#); } @@ -2179,38 +2144,24 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab(1) + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: str): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ + | -- | "); } @@ -2248,38 +2199,24 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab("hello") + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab("hello") - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: str): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab("hello") - | ^^ + | -- | "#); } @@ -2317,38 +2254,24 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab(1, 2) + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int, b: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, 2) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: int): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, 2) - | ^^ + | -- | "); } @@ -2386,38 +2309,24 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab(1) + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int, b: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: int): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ + | -- | "); } @@ -2458,57 +2367,29 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule.pyi:5:5 - | - 4 | @overload - 5 | def ab(a: int): ... - | ^^ - 6 | - 7 | @overload - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, b=2) - | ^^ + | ^^ Clicking here | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 + info: Found 3 declarations + --> mymodule.pyi:5:5 | + 4 | @overload + 5 | def ab(a: int): ... + | -- + 6 | 7 | @overload 8 | def ab(a: int, *, b: int): ... - | ^^ + | -- 9 | - 10 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, b=2) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:11:5 - | 10 | @overload 11 | def ab(a: int, *, c: int): ... - | ^^ + | -- | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, b=2) - | ^^ - | "); } @@ -2548,57 +2429,29 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule.pyi:5:5 - | - 4 | @overload - 5 | def ab(a: int): ... - | ^^ - 6 | - 7 | @overload - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, c=2) - | ^^ + | ^^ Clicking here | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 + info: Found 3 declarations + --> mymodule.pyi:5:5 | + 4 | @overload + 5 | def ab(a: int): ... + | -- + 6 | 7 | @overload 8 | def ab(a: int, *, b: int): ... - | ^^ + | -- 9 | - 10 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, c=2) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:11:5 - | 10 | @overload 11 | def ab(a: int, *, c: int): ... - | ^^ + | -- | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, c=2) - | ^^ - | "); } @@ -2627,21 +2480,21 @@ def ab(a: int, *, c: int): ... // which is correct but unhelpful. Unfortunately even if it only claimed the LHS identifier it // would highlight `subpkg.submod` which is strictly better but still isn't what we want. assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/__init__.py:2:1 - | - 2 | from .subpkg.submod import val - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 3 | - 4 | x = subpkg - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = subpkg - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 declaration + --> mypackage/__init__.py:2:1 + | + 2 | from .subpkg.submod import val + | ------------------------------ + 3 | + 4 | x = subpkg | "); } @@ -2671,18 +2524,18 @@ def ab(a: int, *, c: int): ... // `subpkg = mypackage.subpkg`. As in, it's both defining a local `subpkg` and // loading the module `mypackage.subpkg`, so, it's understandable to get confused! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:1:1 - | - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:7 | 2 | from .subpkg.submod import val - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 declaration + --> mypackage/subpkg/__init__.py:1:1 + | + | "); } @@ -2732,21 +2585,21 @@ def ab(a: int, *, c: int): ... // Going to the submod module is correct! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/submod.py:1:1 - | - 1 | - | ^ - 2 | val: int = 0 - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:14 | 2 | from .subpkg.submod import val - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = submod | + info: Found 1 declaration + --> mypackage/subpkg/submod.py:1:1 + | + 1 | + | - + 2 | val: int = 0 + | "); } @@ -2771,21 +2624,21 @@ def ab(a: int, *, c: int): ... // Going to the subpkg module is correct! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:1:1 - | - 1 | - | ^ - 2 | subpkg: int = 10 - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:7 | 2 | from .subpkg import subpkg - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 declaration + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - + 2 | subpkg: int = 10 + | "); } @@ -2810,20 +2663,20 @@ def ab(a: int, *, c: int): ... // Going to the subpkg `int` is correct! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:2:1 - | - 2 | subpkg: int = 10 - | ^^^^^^ - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:21 | 2 | from .subpkg import subpkg - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 declaration + --> mypackage/subpkg/__init__.py:2:1 + | + 2 | subpkg: int = 10 + | ------ + | "); } @@ -2860,36 +2713,26 @@ def ab(a: int, *, c: int): ... // that *immediately* overwrites the `ImportFromSubmodule`'s definition // This span seemingly doesn't appear at all!? Is it getting hidden by the LHS span? assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ Clicking here + | + info: Found 2 declarations --> mypackage/__init__.py:2:1 | 2 | from .subpkg import subpkg - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | -------------------------- 3 | 4 | x = subpkg | - info: Source - --> mypackage/__init__.py:4:5 - | - 2 | from .subpkg import subpkg - 3 | - 4 | x = subpkg - | ^^^^^^ - | - - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:2:1 + ::: mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 - | ^^^^^^ - | - info: Source - --> mypackage/__init__.py:4:5 - | - 2 | from .subpkg import subpkg - 3 | - 4 | x = subpkg - | ^^^^^^ + | ------ | "); } @@ -2913,63 +2756,29 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:6:7 + | + 4 | a: int = 10 + 5 | + 6 | print(a) + | ^ Clicking here + 7 | + 8 | a: bool = True + | + info: Found 3 declarations --> main.py:2:1 | 2 | a: str = "test" - | ^ + | - 3 | 4 | a: int = 10 - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 + | - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-declaration]: Declaration - --> main.py:4:1 - | - 2 | a: str = "test" - 3 | - 4 | a: int = 10 - | ^ - 5 | - 6 | print(a) - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-declaration]: Declaration - --> main.py:8:1 - | 6 | print(a) 7 | 8 | a: bool = True - | ^ - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True + | - | "#); } @@ -2985,47 +2794,10 @@ def ab(a: int, *, c: int): ... return "No declarations found".to_string(); } - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoDeclarationDiagnostic::new(source, &target)), - ) - } - } - - struct GotoDeclarationDiagnostic { - source: FileRange, - target: FileRange, - } - - impl GotoDeclarationDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } - } - } - - impl IntoDiagnostic for GotoDeclarationDiagnostic { - fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - - let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-declaration")), - Severity::Info, - "Declaration".to_string(), - ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); - - main + self.render_diagnostics([crate::goto_definition::test::GotoDiagnostic::new( + crate::goto_definition::test::GotoAction::Declaration, + targets, + )]) } } } diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 00e08957cb..f37a107c46 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -30,15 +30,15 @@ pub fn goto_definition( } #[cfg(test)] -mod test { +pub(super) mod test { + use crate::tests::{CursorTest, IntoDiagnostic}; - use crate::{NavigationTarget, goto_definition}; + use crate::{NavigationTargets, RangedValue, goto_definition}; use insta::assert_snapshot; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }; - use ruff_db::files::FileRange; use ruff_text_size::Ranged; /// goto-definition on a module should go to the .py not the .pyi @@ -70,19 +70,19 @@ def my_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:1:1 - | - 1 | - | ^ - 2 | def my_function(): - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> main.py:2:6 | 2 | from mymodule import my_function - | ^^^^^^^^ + | ^^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:1:1 + | + 1 | + | - + 2 | def my_function(): + 3 | return "hello" | "#); } @@ -114,20 +114,20 @@ def my_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:1:1 - | - 1 | - | ^ - 2 | def my_function(): - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> main.py:3:5 | 2 | import mymodule 3 | x = mymodule - | ^^^^^^^^ + | ^^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:1:1 + | + 1 | + | - + 2 | def my_function(): + 3 | return "hello" | "#); } @@ -164,19 +164,19 @@ def other_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> main.py:3:7 | 2 | from mymodule import my_function 3 | print(my_function()) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ----------- + 3 | return "hello" | "#); } @@ -206,21 +206,21 @@ def other_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> mymodule.pyi:2:5 | 2 | def my_function(): ... - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here 3 | 4 | def other_function(): ... | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ----------- + 3 | return "hello" + | "#); } @@ -266,54 +266,28 @@ def other_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:3:7 + | + 2 | from mymodule import my_function + 3 | print(my_function()) + | ^^^^^^^^^^^ Clicking here + | + info: Found 3 definitions --> mymodule.py:2:5 | 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source - --> main.py:3:7 - | - 2 | from mymodule import my_function - 3 | print(my_function()) - | ^^^^^^^^^^^ - | - - info[goto-definition]: Definition - --> mymodule.py:5:5 - | + | ----------- 3 | return "hello" 4 | 5 | def my_function(): - | ^^^^^^^^^^^ - 6 | return "hello again" - | - info: Source - --> main.py:3:7 - | - 2 | from mymodule import my_function - 3 | print(my_function()) - | ^^^^^^^^^^^ - | - - info[goto-definition]: Definition - --> mymodule.py:8:5 - | + | ----------- 6 | return "hello again" 7 | 8 | def my_function(): - | ^^^^^^^^^^^ + | ----------- 9 | return "we can't keep doing this" | - info: Source - --> main.py:3:7 - | - 2 | from mymodule import my_function - 3 | print(my_function()) - | ^^^^^^^^^^^ - | "#); } @@ -353,20 +327,20 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:2:7 - | - 2 | class MyClass: - | ^^^^^^^ - 3 | def __init__(self, val): - 4 | self.val = val - | - info: Source + info[goto-definition]: Go to definition --> main.py:3:5 | 2 | from mymodule import MyClass 3 | x = MyClass - | ^^^^^^^ + | ^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:7 + | + 2 | class MyClass: + | ------- + 3 | def __init__(self, val): + 4 | self.val = val | "); } @@ -400,21 +374,21 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:2:7 - | - 2 | class MyClass: - | ^^^^^^^ - 3 | def __init__(self, val): - 4 | self.val = val - | - info: Source + info[goto-definition]: Go to definition --> mymodule.pyi:2:7 | 2 | class MyClass: - | ^^^^^^^ + | ^^^^^^^ Clicking here 3 | def __init__(self, val: bool): ... | + info: Found 1 definition + --> mymodule.py:2:7 + | + 2 | class MyClass: + | ------- + 3 | def __init__(self, val): + 4 | self.val = val + | "); } @@ -454,37 +428,22 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:3:5 + | + 2 | from mymodule import MyClass + 3 | x = MyClass(0) + | ^^^^^^^ Clicking here + | + info: Found 2 definitions --> mymodule.py:2:7 | 2 | class MyClass: - | ^^^^^^^ + | ------- 3 | def __init__(self, val): + | -------- 4 | self.val = val | - info: Source - --> main.py:3:5 - | - 2 | from mymodule import MyClass - 3 | x = MyClass(0) - | ^^^^^^^ - | - - info[goto-definition]: Definition - --> mymodule.py:3:9 - | - 2 | class MyClass: - 3 | def __init__(self, val): - | ^^^^^^^^ - 4 | self.val = val - | - info: Source - --> main.py:3:5 - | - 2 | from mymodule import MyClass - 3 | x = MyClass(0) - | ^^^^^^^ - | "); } @@ -528,22 +487,22 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:5:9 - | - 3 | def __init__(self, val): - 4 | self.val = val - 5 | def action(self): - | ^^^^^^ - 6 | print(self.val) - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:3 | 2 | from mymodule import MyClass 3 | x = MyClass(0) 4 | x.action() - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:5:9 + | + 3 | def __init__(self, val): + 4 | self.val = val + 5 | def action(self): + | ------ + 6 | print(self.val) | "); } @@ -587,22 +546,22 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:3:13 + | + 2 | from mymodule import MyClass + 3 | x = MyClass.action() + | ^^^^^^ Clicking here + | + info: Found 1 definition --> mymodule.py:5:9 | 3 | def __init__(self, val): 4 | self.val = val 5 | def action(): - | ^^^^^^ + | ------ 6 | print("hi!") | - info: Source - --> main.py:3:13 - | - 2 | from mymodule import MyClass - 3 | x = MyClass.action() - | ^^^^^^ - | "#); } @@ -631,17 +590,17 @@ class MyClass: ... .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:2:7 - | - 2 | class MyClass: ... - | ^^^^^^^ - | - info: Source + info[goto-definition]: Go to definition --> main.py:2:22 | 2 | from mymodule import MyClass - | ^^^^^^^ + | ^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:7 + | + 2 | class MyClass: ... + | ------- | "); } @@ -665,22 +624,22 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:2:13 - | - 2 | def my_func(ab, y, z = None): ... - | ^^ - 3 | def my_other_func(ab, y): ... - | - info: Source + info[goto-definition]: Go to definition --> main.py:5:23 | 3 | def my_other_func(ab, y): ... 4 | 5 | my_other_func(my_func(ab=5, y=2), 0) - | ^^ + | ^^ Clicking here 6 | my_func(my_other_func(ab=5, y=2), 0) | + info: Found 1 definition + --> main.py:2:13 + | + 2 | def my_func(ab, y, z = None): ... + | -- + 3 | def my_other_func(ab, y): ... + | "); } @@ -703,21 +662,21 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:19 - | - 2 | def my_func(ab, y, z = None): ... - 3 | def my_other_func(ab, y): ... - | ^^ - 4 | - 5 | my_other_func(my_func(ab=5, y=2), 0) - | - info: Source + info[goto-definition]: Go to definition --> main.py:6:23 | 5 | my_other_func(my_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 + | + 2 | def my_func(ab, y, z = None): ... + 3 | def my_other_func(ab, y): ... + | -- + 4 | + 5 | my_other_func(my_func(ab=5, y=2), 0) | "); } @@ -741,22 +700,22 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:2:13 - | - 2 | def my_func(ab, y): ... - | ^^ - 3 | def my_other_func(ab, y): ... - | - info: Source + info[goto-definition]: Go to definition --> main.py:5:23 | 3 | def my_other_func(ab, y): ... 4 | 5 | my_other_func(my_func(ab=5, y=2), 0) - | ^^ + | ^^ Clicking here 6 | my_func(my_other_func(ab=5, y=2), 0) | + info: Found 1 definition + --> main.py:2:13 + | + 2 | def my_func(ab, y): ... + | -- + 3 | def my_other_func(ab, y): ... + | "); } @@ -779,21 +738,21 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:19 - | - 2 | def my_func(ab, y): ... - 3 | def my_other_func(ab, y): ... - | ^^ - 4 | - 5 | my_other_func(my_func(ab=5, y=2), 0) - | - info: Source + info[goto-definition]: Go to definition --> main.py:6:23 | 5 | my_other_func(my_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 + | + 2 | def my_func(ab, y): ... + 3 | def my_other_func(ab, y): ... + | -- + 4 | + 5 | my_other_func(my_func(ab=5, y=2), 0) | "); } @@ -831,20 +790,20 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a): + | -- + 3 | """the real implementation!""" | "#); } @@ -882,20 +841,20 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab("hello") - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a): + | -- + 3 | """the real implementation!""" | "#); } @@ -933,20 +892,20 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, b = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, 2) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, b = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -984,20 +943,20 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, b = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, b = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -1038,20 +997,20 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, *, b = None, c = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, b=2) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, *, b = None, c = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -1092,20 +1051,20 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, *, b = None, c = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, c=2) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, *, b = None, c = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -1130,22 +1089,22 @@ a + b .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __add__(self, other): - | ^^^^^^^ - 4 | return Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:10:3 | 8 | b = Test() 9 | 10 | a + b - | ^ + | ^ Clicking here | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __add__(self, other): + | ------- + 4 | return Test() + | "); } @@ -1167,21 +1126,21 @@ B() + A() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class A: - 3 | def __radd__(self, other) -> A: - | ^^^^^^^^ - 4 | return self - | - info: Source + info[goto-definition]: Go to definition --> main.py:8:5 | 6 | class B: ... 7 | 8 | B() + A() - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class A: + 3 | def __radd__(self, other) -> A: + | -------- + 4 | return self | "); } @@ -1206,22 +1165,22 @@ a+b .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __add__(self, other): - | ^^^^^^^ - 4 | return Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:10:2 | 8 | b = Test() 9 | 10 | a+b - | ^ + | ^ Clicking here | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __add__(self, other): + | ------- + 4 | return Test() + | "); } @@ -1245,22 +1204,22 @@ a+b .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:8:1 - | - 7 | a = Test() - 8 | b = Test() - | ^ - 9 | - 10 | a+b - | - info: Source + info[goto-definition]: Go to definition --> main.py:10:3 | 8 | b = Test() 9 | 10 | a+b - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:8:1 + | + 7 | a = Test() + 8 | b = Test() + | - + 9 | + 10 | a+b | "); } @@ -1304,22 +1263,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __invert__(self) -> 'Test': ... - | ^^^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | ~a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self) -> 'Test': ... + | ---------- + 4 | + 5 | a = Test() | "); } @@ -1342,22 +1301,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __invert__(self, extra_arg) -> 'Test': ... - | ^^^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | ~a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self, extra_arg) -> 'Test': ... + | ---------- + 4 | + 5 | a = Test() | "); } @@ -1379,22 +1338,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __invert__(self) -> 'Test': ... - | ^^^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | ~ a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self) -> 'Test': ... + | ---------- + 4 | + 5 | a = Test() | "); } @@ -1416,23 +1375,23 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:5:1 - | - 3 | def __invert__(self) -> 'Test': ... - 4 | - 5 | a = Test() - | ^ - 6 | - 7 | -a - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:2 | 5 | a = Test() 6 | 7 | -a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:5:1 + | + 3 | def __invert__(self) -> 'Test': ... + 4 | + 5 | a = Test() + | - + 6 | + 7 | -a | "); } @@ -1454,22 +1413,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __bool__(self) -> bool: ... - | ^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __bool__(self) -> bool: ... + | -------- + 4 | + 5 | a = Test() | "); } @@ -1491,22 +1450,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __len__(self) -> 42: ... - | ^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __len__(self) -> 42: ... + | ------- + 4 | + 5 | a = Test() | "); } @@ -1532,21 +1491,21 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __bool__(self, extra_arg) -> bool: ... - | ^^^^^^^^ - 4 | def __len__(self) -> 42: ... - | - info: Source + info[goto-definition]: Go to definition --> main.py:8:1 | 6 | a = Test() 7 | 8 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __bool__(self, extra_arg) -> bool: ... + | -------- + 4 | def __len__(self) -> 42: ... | "); } @@ -1572,22 +1531,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __len__(self, extra_arg) -> 42: ... - | ^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __len__(self, extra_arg) -> 42: ... + | ------- + 4 | + 5 | a = Test() | "); } @@ -1604,36 +1563,28 @@ a: float = 3.14 .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:2:4 + | + 2 | a: float = 3.14 + | ^^^^^ Clicking here + | + info: Found 2 definitions --> stdlib/builtins.pyi:348:7 | 347 | @disjoint_base 348 | class int: - | ^^^ + | --- 349 | """int([x]) -> integer 350 | int(x, base=10) -> integer | - info: Source - --> main.py:2:4 - | - 2 | a: float = 3.14 - | ^^^^^ - | - - info[goto-definition]: Definition - --> stdlib/builtins.pyi:661:7 + ::: stdlib/builtins.pyi:661:7 | 660 | @disjoint_base 661 | class float: - | ^^^^^ + | ----- 662 | """Convert a string or number to a floating-point number, if possible.""" | - info: Source - --> main.py:2:4 - | - 2 | a: float = 3.14 - | ^^^^^ - | "#); } @@ -1649,51 +1600,35 @@ a: complex = 3.14 .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:2:4 + | + 2 | a: complex = 3.14 + | ^^^^^^^ Clicking here + | + info: Found 3 definitions --> stdlib/builtins.pyi:348:7 | 347 | @disjoint_base 348 | class int: - | ^^^ + | --- 349 | """int([x]) -> integer 350 | int(x, base=10) -> integer | - info: Source - --> main.py:2:4 - | - 2 | a: complex = 3.14 - | ^^^^^^^ - | - - info[goto-definition]: Definition - --> stdlib/builtins.pyi:661:7 + ::: stdlib/builtins.pyi:661:7 | 660 | @disjoint_base 661 | class float: - | ^^^^^ + | ----- 662 | """Convert a string or number to a floating-point number, if possible.""" | - info: Source - --> main.py:2:4 - | - 2 | a: complex = 3.14 - | ^^^^^^^ - | - - info[goto-definition]: Definition - --> stdlib/builtins.pyi:822:7 + ::: stdlib/builtins.pyi:822:7 | 821 | @disjoint_base 822 | class complex: - | ^^^^^^^ + | ------- 823 | """Create a complex number from a string or numbers. | - info: Source - --> main.py:2:4 - | - 2 | a: complex = 3.14 - | ^^^^^^^ - | "#); } @@ -1733,63 +1668,29 @@ TracebackType .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:6:7 + | + 4 | a: int = 10 + 5 | + 6 | print(a) + | ^ Clicking here + 7 | + 8 | a: bool = True + | + info: Found 3 definitions --> main.py:2:1 | 2 | a: str = "test" - | ^ + | - 3 | 4 | a: int = 10 - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 + | - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-definition]: Definition - --> main.py:4:1 - | - 2 | a: str = "test" - 3 | - 4 | a: int = 10 - | ^ - 5 | - 6 | print(a) - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-definition]: Definition - --> main.py:8:1 - | 6 | print(a) 7 | 8 | a: bool = True - | ^ - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True + | - | "#); } @@ -1805,47 +1706,86 @@ TracebackType return "No definitions found".to_string(); } - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoDefinitionDiagnostic::new(source, &target)), - ) + self.render_diagnostics([GotoDiagnostic::new(GotoAction::Definition, targets)]) } } - struct GotoDefinitionDiagnostic { - source: FileRange, - target: FileRange, + pub(crate) struct GotoDiagnostic { + action: GotoAction, + targets: RangedValue, } - impl GotoDefinitionDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } + impl GotoDiagnostic { + pub(crate) fn new(action: GotoAction, targets: RangedValue) -> Self { + Self { action, targets } } } - impl IntoDiagnostic for GotoDefinitionDiagnostic { + impl IntoDiagnostic for GotoDiagnostic { fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - + let source = self.targets.range; let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-definition")), + DiagnosticId::Lint(LintName::of(self.action.name())), Severity::Info, - "Definition".to_string(), + self.action.label().to_string(), ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); + + main.annotate( + Annotation::primary(Span::from(source.file()).with_range(source.range())) + .message("Clicking here"), + ); + + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "Found {} {}{}", + self.targets.len(), + self.action.item_label(), + if self.targets.len() == 1 { "" } else { "s" } + ), + ); + + for target in self.targets { + sub.annotate(Annotation::secondary( + Span::from(target.file()).with_range(target.focus_range()), + )); + } + + main.sub(sub); main } } + + pub(crate) enum GotoAction { + Definition, + Declaration, + TypeDefinition, + } + + impl GotoAction { + fn name(&self) -> &'static str { + match self { + GotoAction::Definition => "goto-definition", + GotoAction::Declaration => "goto-declaration", + GotoAction::TypeDefinition => "goto-type definition", + } + } + + fn label(&self) -> &'static str { + match self { + GotoAction::Definition => "Go to definition", + GotoAction::Declaration => "Go to declaration", + GotoAction::TypeDefinition => "Go to type definition", + } + } + + fn item_label(&self) -> &'static str { + match self { + GotoAction::Definition => "definition", + GotoAction::Declaration => "declaration", + GotoAction::TypeDefinition => "type definition", + } + } + } } diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 53cc98413d..16e6165c86 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -28,15 +28,9 @@ pub fn goto_type_definition( #[cfg(test)] mod tests { - use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; - use crate::{NavigationTarget, goto_type_definition}; + use crate::goto_type_definition; + use crate::tests::{CursorTest, cursor_test}; use insta::assert_snapshot; - use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, - SubDiagnosticSeverity, - }; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; #[test] fn goto_type_of_expression_with_class_type() { @@ -49,21 +43,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:7 - | - 2 | class Test: ... - | ^^^^ - 3 | - 4 | ab = Test() - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | class Test: ... 3 | 4 | ab = Test() - | ^^ + | ^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:7 + | + 2 | class Test: ... + | ---- + 3 | + 4 | ab = Test() | "); } @@ -79,23 +73,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/typing.pyi:351:1 - | - 349 | Final: _SpecialForm - 350 | - 351 | Literal: _SpecialForm - | ^^^^^^^ - 352 | TypedDict: _SpecialForm - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from typing import Literal 3 | 4 | ab = Literal - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/typing.pyi:351:1 + | + 349 | Final: _SpecialForm + 350 | + 351 | Literal: _SpecialForm + | ------- + 352 | TypedDict: _SpecialForm + | "); } @@ -112,23 +106,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/typing.pyi:166:7 - | - 164 | # from _typeshed import AnnotationForm - 165 | - 166 | class Any: - | ^^^ - 167 | """Special type indicating an unconstrained type. - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from typing import Any 3 | 4 | ab = Any - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/typing.pyi:166:7 + | + 164 | # from _typeshed import AnnotationForm + 165 | + 166 | class Any: + | --- + 167 | """Special type indicating an unconstrained type. + | "#); } @@ -144,24 +138,24 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/typing.pyi:781:1 - | - 779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ... - 780 | - 781 | Generic: type[_Generic] - | ^^^^^^^ - 782 | - 783 | class _ProtocolMeta(ABCMeta): - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from typing import Generic 3 | 4 | ab = Generic - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/typing.pyi:781:1 + | + 779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ... + 780 | + 781 | Generic: type[_Generic] + | ------- + 782 | + 783 | class _ProtocolMeta(ABCMeta): + | "); } @@ -176,23 +170,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/ty_extensions.pyi:21:1 - | - 19 | # Types - 20 | Unknown = object() - 21 | AlwaysTruthy = object() - | ^^^^^^^^^^^^ - 22 | AlwaysFalsy = object() - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from ty_extensions import AlwaysTruthy 3 | 4 | ab = AlwaysTruthy - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/ty_extensions.pyi:21:1 + | + 19 | # Types + 20 | Unknown = object() + 21 | AlwaysTruthy = object() + | ------------ + 22 | AlwaysFalsy = object() + | "); } @@ -209,21 +203,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:5 - | - 2 | def foo(a, b): ... - | ^^^ - 3 | - 4 | ab = foo - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:6:1 | 4 | ab = foo 5 | 6 | ab - | ^^ + | ^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:5 + | + 2 | def foo(a, b): ... + | --- + 3 | + 4 | ab = foo | "); } @@ -247,41 +241,25 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:12:1 + | + 10 | a = bar + 11 | + 12 | a + | ^ Clicking here + | + info: Found 2 type definitions --> main.py:3:5 | 3 | def foo(a, b): ... - | ^^^ + | --- 4 | 5 | def bar(a, b): ... - | - info: Source - --> main.py:12:1 - | - 10 | a = bar - 11 | - 12 | a - | ^ - | - - info[goto-type-definition]: Type definition - --> main.py:5:5 - | - 3 | def foo(a, b): ... - 4 | - 5 | def bar(a, b): ... - | ^^^ + | --- 6 | 7 | if random.choice(): | - info: Source - --> main.py:12:1 - | - 10 | a = bar - 11 | - 12 | a - | ^ - | "); } @@ -296,17 +274,17 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:8 | 2 | import lib - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -323,17 +301,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/__init__.py:1:1 - | - 1 | b = 7 - | ^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:8 | 2 | import lib.submod - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib/__init__.py:1:1 + | + 1 | b = 7 + | ----- | "); } @@ -350,17 +328,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/submod.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:12 | 2 | import lib.submod - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 type definition + --> lib/submod.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -376,17 +354,17 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:6 | 2 | from lib import a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -403,17 +381,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/__init__.py:1:1 - | - 1 | b = 7 - | ^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:6 | 2 | from lib.submod import a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib/__init__.py:1:1 + | + 1 | b = 7 + | ----- | "); } @@ -430,17 +408,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/submod.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:10 | 2 | from lib.submod import a - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 type definition + --> lib/submod.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -466,19 +444,19 @@ mod tests { .unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/sub/bot/botmod.py:1:1 - | - 1 | botmod = 31 - | ^^^^^^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> lib/sub/__init__.py:2:11 | 2 | from .bot.botmod import * - | ^^^^^^ + | ^^^^^^ Clicking here 3 | sub = 2 | + info: Found 1 type definition + --> lib/sub/bot/botmod.py:1:1 + | + 1 | botmod = 31 + | ----------- + | "); } @@ -503,19 +481,19 @@ mod tests { .unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/sub/bot/__init__.py:1:1 - | - 1 | bot = 3 - | ^^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> lib/sub/__init__.py:2:7 | 2 | from .bot.botmod import * - | ^^^ + | ^^^ Clicking here 3 | sub = 2 | + info: Found 1 type definition + --> lib/sub/bot/__init__.py:1:1 + | + 1 | bot = 3 + | ------- + | "); } @@ -540,19 +518,19 @@ mod tests { .unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/sub/bot/__init__.py:1:1 - | - 1 | bot = 3 - | ^^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> lib/sub/__init__.py:2:7 | 2 | from .bot.botmod import * - | ^^^ + | ^^^ Clicking here 3 | sub = 2 | + info: Found 1 type definition + --> lib/sub/bot/__init__.py:1:1 + | + 1 | bot = 3 + | ------- + | "); } @@ -592,19 +570,19 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | import lib 3 | 4 | lib - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -620,23 +598,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | a: str = "test" 3 | 4 | a - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } #[test] @@ -648,21 +626,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:10 + | + 2 | a: str = "test" + | ^^^^^^ Clicking here + | + info: Found 1 type definition --> stdlib/builtins.pyi:915:7 | 914 | @disjoint_base 915 | class str(Sequence[str]): - | ^^^ + | --- 916 | """str(object='') -> str 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | - info: Source - --> main.py:2:10 - | - 2 | a: str = "test" - | ^^^^^^ - | "#); } @@ -675,17 +653,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:12 - | - 2 | type Alias[T: int = bool] = list[T] - | ^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:34 | 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] + | - | "); } @@ -699,17 +677,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:14 - | - 2 | type Alias[**P = [int, str]] = Callable[P, int] - | ^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:41 | 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] + | - | "); } @@ -756,23 +734,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:5 + | + 2 | a: "MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 type definition --> main.py:4:7 | 2 | a: "MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:5 - | - 2 | a: "MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -802,41 +780,31 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ - 5 | """some docs""" - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:4 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^ Clicking here 3 | 4 | class MyClass: | - - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:950:11 + info: Found 2 type definitions + --> main.py:4:7 + | + 2 | a: "None | MyClass" = 1 + 3 | + 4 | class MyClass: + | ------- + 5 | """some docs""" + | + ::: stdlib/types.pyi:950:11 | 948 | if sys.version_info >= (3, 10): 949 | @final 950 | class NoneType: - | ^^^^^^^^ + | -------- 951 | """The type of the None singleton.""" | - info: Source - --> main.py:2:4 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -866,41 +834,31 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ - 5 | """some docs""" - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:4 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^ Clicking here 3 | 4 | class MyClass: | - - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:950:11 + info: Found 2 type definitions + --> main.py:4:7 + | + 2 | a: "None | MyClass" = 1 + 3 | + 4 | class MyClass: + | ------- + 5 | """some docs""" + | + ::: stdlib/types.pyi:950:11 | 948 | if sys.version_info >= (3, 10): 949 | @final 950 | class NoneType: - | ^^^^^^^^ + | -------- 951 | """The type of the None singleton.""" | - info: Source - --> main.py:2:4 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -916,23 +874,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:4 + | + 2 | a: "MyClass |" = 1 + | ^^^^^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 type definition --> stdlib/ty_extensions.pyi:20:1 | 19 | # Types 20 | Unknown = object() - | ^^^^^^^ + | ------- 21 | AlwaysTruthy = object() 22 | AlwaysFalsy = object() | - info: Source - --> main.py:2:4 - | - 2 | a: "MyClass |" = 1 - | ^^^^^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -973,21 +931,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:6 + | + 2 | ab: "ab" + | ^^ Clicking here + | + info: Found 1 type definition --> stdlib/ty_extensions.pyi:20:1 | 19 | # Types 20 | Unknown = object() - | ^^^^^^^ + | ------- 21 | AlwaysTruthy = object() 22 | AlwaysFalsy = object() | - info: Source - --> main.py:2:6 - | - 2 | ab: "ab" - | ^^ - | "#); } @@ -1000,21 +958,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:5 + | + 2 | x: "foobar" + | ^^^^^^ Clicking here + | + info: Found 1 type definition --> stdlib/ty_extensions.pyi:20:1 | 19 | # Types 20 | Unknown = object() - | ^^^^^^^ + | ------- 21 | AlwaysTruthy = object() 22 | AlwaysFalsy = object() | - info: Source - --> main.py:2:5 - | - 2 | x: "foobar" - | ^^^^^^ - | "#); } @@ -1160,23 +1118,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:2:7 - | - 2 | class Click: - | ^^^^^ - 3 | __match_args__ = ("position", "button") - 4 | def __init__(self, pos, btn): - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:10:14 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^^^^ + | ^^^^^ Clicking here 11 | x = ab | + info: Found 1 type definition + --> main.py:2:7 + | + 2 | class Click: + | ----- + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | "#); } @@ -1209,17 +1167,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- | "); } @@ -1233,17 +1191,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:13 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:37 | 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]] + | -- | "); } @@ -1305,23 +1263,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:6 | 2 | def test(a: str): ... 3 | 4 | test(a= "123") - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1339,23 +1297,23 @@ mod tests { // the keyword is typed as a string. It's only the passed argument that // is an int. Navigating to `str` would match pyright's behavior. assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:348:7 - | - 347 | @disjoint_base - 348 | class int: - | ^^^ - 349 | """int([x]) -> integer - 350 | int(x, base=10) -> integer - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:6 | 2 | def test(a: str): ... 3 | 4 | test(a= 123) - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | --- + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | "#); } @@ -1372,23 +1330,23 @@ f(**kwargs) ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:2920:7 - | - 2919 | @disjoint_base - 2920 | class dict(MutableMapping[_KT, _VT]): - | ^^^^ - 2921 | """dict() -> new empty dictionary - 2922 | dict(mapping) -> new dictionary initialized from a mapping object's - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:6:5 | 4 | kwargs = { "name": "test"} 5 | 6 | f(**kwargs) - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:2920:7 + | + 2919 | @disjoint_base + 2920 | class dict(MutableMapping[_KT, _VT]): + | ---- + 2921 | """dict() -> new empty dictionary + 2922 | dict(mapping) -> new dictionary initialized from a mapping object's + | "#); } @@ -1410,25 +1368,25 @@ def outer(): // Should find the variable declaration in the outer scope, not the nonlocal statement assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:8:16 | 6 | nonlocal x 7 | x = "modified" 8 | return x # Should find the nonlocal x declaration in outer scope - | ^ + | ^ Clicking here 9 | 10 | return inner | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1467,23 +1425,23 @@ def function(): // Should find the global variable declaration, not the global statement assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:7:12 | 5 | global global_var 6 | global_var = "modified" 7 | return global_var # Should find the global variable declaration - | ^^^^^^^^^^ + | ^^^^^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1514,22 +1472,22 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:3:5 | 2 | def foo(a: str): 3 | a - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1547,20 +1505,20 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:7 - | - 2 | class X: - | ^ - 3 | def foo(a, b): ... - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:7:1 | 5 | x = X() 6 | 7 | x.foo() - | ^ + | ^ Clicking here + | + info: Found 1 type definition + --> main.py:2:7 + | + 2 | class X: + | - + 3 | def foo(a, b): ... | "); } @@ -1576,21 +1534,21 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:5 - | - 2 | def foo(a, b): ... - | ^^^ - 3 | - 4 | foo() - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | def foo(a, b): ... 3 | 4 | foo() - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:5 + | + 2 | def foo(a, b): ... + | --- + 3 | + 4 | foo() | "); } @@ -1606,23 +1564,23 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:15 | 2 | def foo(a: str | None, b): 3 | if a is not None: 4 | print(a) - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1636,39 +1594,30 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:950:11 - | - 948 | if sys.version_info >= (3, 10): - 949 | @final - 950 | class NoneType: - | ^^^^^^^^ - 951 | """The type of the None singleton.""" - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:3:5 | 2 | def foo(a: str | None, b): 3 | a - | ^ + | ^ Clicking here | - - info[goto-type-definition]: Type definition + info: Found 2 type definitions --> stdlib/builtins.pyi:915:7 | 914 | @disjoint_base 915 | class str(Sequence[str]): - | ^^^ + | --- 916 | """str(object='') -> str 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | - info: Source - --> main.py:3:5 - | - 2 | def foo(a: str | None, b): - 3 | a - | ^ - | + ::: stdlib/types.pyi:950:11 + | + 948 | if sys.version_info >= (3, 10): + 949 | @final + 950 | class NoneType: + | -------- + 951 | """The type of the None singleton.""" + | "#); } @@ -1694,18 +1643,18 @@ def function(): // The module is the correct type definition assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = subpkg - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> mypackage/subpkg/__init__.py:1:1 + | + | "); } @@ -1731,18 +1680,18 @@ def function(): // The module is the correct type definition assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:2:7 | 2 | from .subpkg.submod import val - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 type definition + --> mypackage/subpkg/__init__.py:1:1 + | + | "); } @@ -1768,23 +1717,23 @@ def function(): // Unknown is correct, `submod` is not in scope assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/ty_extensions.pyi:20:1 - | - 19 | # Types - 20 | Unknown = object() - | ^^^^^^^ - 21 | AlwaysTruthy = object() - 22 | AlwaysFalsy = object() - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = submod - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/ty_extensions.pyi:20:1 + | + 19 | # Types + 20 | Unknown = object() + | ------- + 21 | AlwaysTruthy = object() + 22 | AlwaysFalsy = object() + | "); } @@ -1810,20 +1759,20 @@ def function(): // The module is correct assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> mypackage/__init__.py:2:14 + | + 2 | from .subpkg.submod import val + | ^^^^^^ Clicking here + 3 | + 4 | x = submod + | + info: Found 1 type definition --> mypackage/subpkg/submod.py:1:1 | 1 | / 2 | | val: int = 0 - | |_____________^ - | - info: Source - --> mypackage/__init__.py:2:14 - | - 2 | from .subpkg.submod import val - | ^^^^^^ - 3 | - 4 | x = submod + | |_____________- | "); } @@ -1849,20 +1798,20 @@ def function(): // The module is correct assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg import subpkg + | ^^^^^^ Clicking here + 3 | + 4 | x = subpkg + | + info: Found 1 type definition --> mypackage/subpkg/__init__.py:1:1 | 1 | / 2 | | subpkg: int = 10 - | |_________________^ - | - info: Source - --> mypackage/__init__.py:2:7 - | - 2 | from .subpkg import subpkg - | ^^^^^^ - 3 | - 4 | x = subpkg + | |_________________- | "); } @@ -1888,23 +1837,23 @@ def function(): // `int` is correct assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^^^^ Clicking here + 3 | + 4 | x = subpkg + | + info: Found 1 type definition --> stdlib/builtins.pyi:348:7 | 347 | @disjoint_base 348 | class int: - | ^^^ + | --- 349 | """int([x]) -> integer 350 | int(x, base=10) -> integer | - info: Source - --> mypackage/__init__.py:2:21 - | - 2 | from .subpkg import subpkg - | ^^^^^^ - 3 | - 4 | x = subpkg - | "#); } @@ -1929,23 +1878,23 @@ def function(): // `int` is correct assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:348:7 - | - 347 | @disjoint_base - 348 | class int: - | ^^^ - 349 | """int([x]) -> integer - 350 | int(x, base=10) -> integer - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:4:5 | 2 | from .subpkg import subpkg 3 | 4 | x = subpkg - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | --- + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | "#); } @@ -1961,47 +1910,10 @@ def function(): return "No type definitions found".to_string(); } - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), - ) - } - } - - struct GotoTypeDefinitionDiagnostic { - source: FileRange, - target: FileRange, - } - - impl GotoTypeDefinitionDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } - } - } - - impl IntoDiagnostic for GotoTypeDefinitionDiagnostic { - fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - - let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-type-definition")), - Severity::Info, - "Type definition".to_string(), - ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); - - main + self.render_diagnostics([crate::goto_definition::test::GotoDiagnostic::new( + crate::goto_definition::test::GotoAction::TypeDefinition, + targets, + )]) } } } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 28989dcf8f..7d65984f8e 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -230,6 +230,11 @@ impl NavigationTargets { fn is_empty(&self) -> bool { self.0.is_empty() } + + #[cfg(test)] + fn len(&self) -> usize { + self.0.len() + } } impl IntoIterator for NavigationTargets { From 5a9d6a91ea836fbaf14c9be3e8bb01a238f1158d Mon Sep 17 00:00:00 2001 From: Luca Chiodini Date: Thu, 11 Dec 2025 16:03:55 +0100 Subject: [PATCH 27/36] [ty] Uniformly use "not supported" in diagnostics (#21916) --- .../resources/mdtest/assignment/augmented.md | 4 +- .../resources/mdtest/binary/classes.md | 2 +- .../resources/mdtest/binary/custom.md | 90 +++++++++---------- .../resources/mdtest/binary/instances.md | 4 +- .../resources/mdtest/binary/integers.md | 2 +- .../resources/mdtest/binary/unions.md | 6 +- .../mdtest/conditional/if_expression.md | 2 +- .../mdtest/conditional/if_statement.md | 4 +- .../resources/mdtest/conditional/match.md | 2 +- .../resources/mdtest/expression/assert.md | 2 +- .../resources/mdtest/expression/boolean.md | 6 +- .../mdtest/generics/legacy/functions.md | 4 +- .../mdtest/generics/pep695/functions.md | 4 +- .../resources/mdtest/implicit_type_aliases.md | 2 +- .../resources/mdtest/loops/while_loop.md | 2 +- .../resources/mdtest/narrow/truthiness.md | 2 +- ...-…_-_Earlier_versions_(f2859c9800f37c7).snap | 2 +- ...perations_involving…_(492b1163b8163c05).snap | 2 +- ...eturn_type_that_doe…_(feccf6b9da1e7cd3).snap | 4 +- ...ect_that_implemen…_(ab3f546bf004e24d).snap | 2 +- ...hained_comparisons_…_(c391c13e2abc18a0).snap | 4 +- ...ined_comparisons_…_(f45f1da2f8ca693d).snap | 2 +- ...ality_with_elemen…_(39b614d4707c0661).snap | 2 +- ..._Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap | 2 +- ..._Has_a_`__bool__`_met…_(15636dc4074e5335).snap | 2 +- ..._Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap | 2 +- ...-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap | 2 +- .../resources/mdtest/ty_extensions.md | 2 +- .../resources/mdtest/unary/custom.md | 54 +++++------ .../resources/mdtest/unary/invert_add_usub.md | 6 +- .../resources/mdtest/unary/not.md | 2 +- crates/ty_python_semantic/src/types.rs | 10 +-- .../src/types/infer/builder.rs | 6 +- 33 files changed, 122 insertions(+), 122 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index d800c86c6a..cec8c6de43 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -44,7 +44,7 @@ class C: return 42 x = C() -# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`" +# error: [unsupported-operator] "Operator `-=` is not supported between objects of type `C` and `Literal[1]`" x -= 1 reveal_type(x) # revealed: int @@ -79,7 +79,7 @@ def _(flag: bool): f = Foo() - # error: [unsupported-operator] "Operator `+=` is unsupported between objects of type `Foo` and `Literal["Hello, world!"]`" + # error: [unsupported-operator] "Operator `+=` is not supported between objects of type `Foo` and `Literal["Hello, world!"]`" f += "Hello, world!" reveal_type(f) # revealed: int | Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/binary/classes.md b/crates/ty_python_semantic/resources/mdtest/binary/classes.md index 4a3580a8de..e0da23bf50 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/classes.md @@ -27,7 +27,7 @@ python-version = "3.9" class A: ... class B: ... -# error: "Operator `|` is unsupported between objects of type `` and ``" +# error: "Operator `|` is not supported between objects of type `` and ``" reveal_type(A | B) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md index d2587b7a75..9bd0852253 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -79,59 +79,59 @@ reveal_type(Sub() & Sub()) # revealed: Literal["&"] reveal_type(Sub() // Sub()) # revealed: Literal["//"] # No does not implement any of the dunder methods. -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `No` and `No`" reveal_type(No() + No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `No` and `No`" reveal_type(No() - No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `*` is not supported between objects of type `No` and `No`" reveal_type(No() * No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `@` is not supported between objects of type `No` and `No`" reveal_type(No() @ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `/` is not supported between objects of type `No` and `No`" reveal_type(No() / No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `%` is not supported between objects of type `No` and `No`" reveal_type(No() % No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `**` is not supported between objects of type `No` and `No`" reveal_type(No() ** No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `No` and `No`" reveal_type(No() << No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `No` and `No`" reveal_type(No() >> No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `|` is not supported between objects of type `No` and `No`" reveal_type(No() | No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `^` is not supported between objects of type `No` and `No`" reveal_type(No() ^ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `&` is not supported between objects of type `No` and `No`" reveal_type(No() & No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `//` is not supported between objects of type `No` and `No`" reveal_type(No() // No()) # revealed: Unknown # Yes does not implement any of the reflected dunder methods. -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `No` and `Yes`" reveal_type(No() + Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `No` and `Yes`" reveal_type(No() - Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `*` is not supported between objects of type `No` and `Yes`" reveal_type(No() * Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `@` is not supported between objects of type `No` and `Yes`" reveal_type(No() @ Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `/` is not supported between objects of type `No` and `Yes`" reveal_type(No() / Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `%` is not supported between objects of type `No` and `Yes`" reveal_type(No() % Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `**` is not supported between objects of type `No` and `Yes`" reveal_type(No() ** Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `No` and `Yes`" reveal_type(No() << Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `No` and `Yes`" reveal_type(No() >> Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `|` is not supported between objects of type `No` and `Yes`" reveal_type(No() | Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `^` is not supported between objects of type `No` and `Yes`" reveal_type(No() ^ Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `&` is not supported between objects of type `No` and `Yes`" reveal_type(No() & Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `//` is not supported between objects of type `No` and `Yes`" reveal_type(No() // Yes()) # revealed: Unknown ``` @@ -307,11 +307,11 @@ class Yes: class Sub(Yes): ... class No: ... -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" reveal_type(Yes + Yes) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" reveal_type(Sub + Sub) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" reveal_type(No + No) # revealed: Unknown ``` @@ -336,11 +336,11 @@ def sub() -> type[Sub]: def no() -> type[No]: return No -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[Yes]` and `type[Yes]`" reveal_type(yes() + yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[Sub]` and `type[Sub]`" reveal_type(sub() + sub()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[No]` and `type[No]`" reveal_type(no() + no()) # revealed: Unknown ``` @@ -350,30 +350,30 @@ reveal_type(no() + no()) # revealed: Unknown def f(): pass -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f + f) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f - f) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `*` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f * f) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `@` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f @ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `/` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f / f) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `%` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f % f) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `**` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f**f) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f << f) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f >> f) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `|` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f | f) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `^` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f ^ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `&` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f & f) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `//` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f // f) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index c981a570b0..c1cc6f924e 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -386,7 +386,7 @@ class A(metaclass=Meta): ... class B(metaclass=Meta): ... reveal_type(A + B) # revealed: int -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `` and ``" reveal_type(A - B) # revealed: Unknown reveal_type(A < B) # revealed: bool @@ -412,7 +412,7 @@ class A: def __init__(self): self.__add__ = add_impl -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `A` and `A`" # revealed: Unknown reveal_type(A() + A()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md index a021a15ae1..401c09d756 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/integers.md @@ -13,7 +13,7 @@ reveal_type(3 | 4) # revealed: Literal[7] reveal_type(5 & 6) # revealed: Literal[4] reveal_type(7 ^ 2) # revealed: Literal[5] -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `Literal[2]` and `Literal["f"]`" reveal_type(2 + "f") # revealed: Unknown def lhs(x: int): diff --git a/crates/ty_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md index 1ec0794cc4..1b980170d4 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/unions.md @@ -5,9 +5,9 @@ combinations of types: ```py def f1(i: int, u: int | None): - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int` and `int | None`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int` and `int | None`" reveal_type(i + u) # revealed: Unknown - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | None` and `int`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | None` and `int`" reveal_type(u + i) # revealed: Unknown ``` @@ -18,7 +18,7 @@ cannot be added, because that would require addition of `int` and `str` or vice def f2(i: int, s: str, int_or_str: int | str): i + i s + s - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" reveal_type(int_or_str + int_or_str) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md index 48b912cce1..082e0d43db 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md @@ -42,6 +42,6 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" 3 if NotBoolable() else 4 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md index c7a8c7732b..f55dc41160 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md @@ -154,10 +154,10 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" elif NotBoolable(): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/match.md b/crates/ty_python_semantic/resources/mdtest/conditional/match.md index c0df166e03..492ca8ef53 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/match.md @@ -378,7 +378,7 @@ class NotBoolable: def _(target: int, flag: NotBoolable): y = 1 match target: - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" case 1 if flag: y = 2 case 2: diff --git a/crates/ty_python_semantic/resources/mdtest/expression/assert.md b/crates/ty_python_semantic/resources/mdtest/expression/assert.md index ddb429a576..6fd9e7700f 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/assert.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/assert.md @@ -4,6 +4,6 @@ class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" assert NotBoolable() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md index 413acf8e39..67ba0f4f05 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -232,7 +232,7 @@ if NotBoolable(): class NotBoolable: __bool__: None = None -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... ``` @@ -244,7 +244,7 @@ def test(cond: bool): class NotBoolable: __bool__: int | None = None if cond else 3 - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... ``` @@ -258,7 +258,7 @@ def test(cond: bool): a = 10 if cond else NotBoolable() - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `Literal[10] | NotBoolable`" if a: ... ``` 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 69e079ca65..f883d1fb1a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -277,7 +277,7 @@ T = TypeVar("T", int, str) def same_constrained_types(t1: T, t2: T) -> T: # TODO: no error - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" return t1 + t2 ``` @@ -287,7 +287,7 @@ and an `int` and a `str` cannot be added together: ```py def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" return t1 + t2 ``` 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 eedea0beaa..cb8f7dbc90 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -246,7 +246,7 @@ methods that are compatible with the return type, so the `return` expression is ```py def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: # TODO: no error - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" return t1 + t2 ``` @@ -256,7 +256,7 @@ and an `int` and a `str` cannot be added together: ```py def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" return t1 + t2 ``` 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 0886393143..99a5de8aa9 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -214,7 +214,7 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt): `NoneType` has no special or-operator behavior, so this is an error: ```py -None | None # error: [unsupported-operator] "Operator `|` is unsupported between objects of type `None` and `None`" +None | None # error: [unsupported-operator] "Operator `|` is not supported between objects of type `None` and `None`" ``` When constructing something nonsensical like `int | 1`, we emit a diagnostic for the expression diff --git a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md index 5a5784b85d..41e48b1404 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md @@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" while NotBoolable(): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index d6df7c3276..cb9f0c4545 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -270,7 +270,7 @@ def _( if af: reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MetaDeferred`" if d: # TODO: Should be `Unknown` reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap index 3cfe98c8f7..83b964676a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap @@ -19,7 +19,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/assignment/annotations.m # Diagnostics ``` -error[unsupported-operator]: Operator `|` is unsupported between objects of type `` and `` +error[unsupported-operator]: Operator `|` is not supported between objects of type `` and `` --> src/mdtest_snippet.py:2:12 | 1 | # error: [unsupported-operator] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap index ac85e00fa0..2fba47e078 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap @@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/binary/instances.md # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap index d6698b03f4..26415ace68 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap @@ -28,7 +28,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/mem # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] @@ -43,7 +43,7 @@ info: rule `unsupported-bool-conversion` is enabled by default ``` ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:11:1 | 9 | 10 in WithContains() diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap index defb8528ec..f5e88ebed0 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap @@ -22,7 +22,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/unary/not.md # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:5:1 | 4 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap index e40ecc8361..f62a90156b 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap @@ -33,7 +33,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/ric # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:12:1 | 11 | # error: [unsupported-bool-conversion] @@ -48,7 +48,7 @@ info: rule `unsupported-bool-conversion` is enabled by default ``` ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:14:1 | 12 | 10 < Comparable() < 20 diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap index e8fe6a5285..52bb93b61e 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap @@ -34,7 +34,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable | Literal[False]` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable | Literal[False]` --> src/mdtest_snippet.py:15:1 | 14 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap index 2c9fbd885a..c530be195b 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap @@ -58,7 +58,7 @@ info: rule `invalid-method-override` is enabled by default ``` ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:10:1 | 9 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap index 343aaccc77..bcb4ffb0c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap @@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap index 09343ef5d6..b3a86fac93 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap index 9957e4c64f..0e35519ee4 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap index 22d0cc6ada..d86b17b1dc 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap @@ -32,7 +32,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly +error[unsupported-bool-conversion]: Boolean conversion is not supported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly --> src/mdtest_snippet.py:15:8 | 14 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 9cb9ca40f4..09315697da 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -237,7 +237,7 @@ class InvalidBoolDunder: def __bool__(self) -> int: return 1 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `InvalidBoolDunder`" static_assert(InvalidBoolDunder()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md index 1544e42890..2b2eb3a619 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -24,11 +24,11 @@ reveal_type(+Sub()) # revealed: bool reveal_type(-Sub()) # revealed: str reveal_type(~Sub()) # revealed: int -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `No`" reveal_type(+No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `No`" reveal_type(-No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `No`" reveal_type(~No()) # revealed: Unknown ``` @@ -52,25 +52,25 @@ class Yes: class Sub(Yes): ... class No: ... -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~No) # revealed: Unknown ``` @@ -80,11 +80,11 @@ reveal_type(~No) # revealed: Unknown def f(): pass -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `def f() -> Unknown`" reveal_type(+f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `def f() -> Unknown`" reveal_type(-f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `def f() -> Unknown`" reveal_type(~f) # revealed: Unknown ``` @@ -113,25 +113,25 @@ def sub() -> type[Sub]: def no() -> type[No]: return No -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[Yes]`" reveal_type(+yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[Yes]`" reveal_type(-yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[Yes]`" reveal_type(~yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[Sub]`" reveal_type(+sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[Sub]`" reveal_type(-sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[Sub]`" reveal_type(~sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[No]`" reveal_type(+no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[No]`" reveal_type(-no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[No]`" reveal_type(~no()) # revealed: Unknown ``` @@ -160,10 +160,10 @@ reveal_type(+Sub) # revealed: bool reveal_type(-Sub) # revealed: str reveal_type(~Sub) # revealed: int -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~No) # revealed: Unknown ``` 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 6b07cb1d2e..100176e1cb 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 @@ -27,7 +27,7 @@ reveal_type(~a) # revealed: Literal[True] class NoDunder: ... b = NoDunder() -+b # error: [unsupported-operator] "Unary operator `+` is unsupported for type `NoDunder`" --b # error: [unsupported-operator] "Unary operator `-` is unsupported for type `NoDunder`" -~b # error: [unsupported-operator] "Unary operator `~` is unsupported for type `NoDunder`" ++b # error: [unsupported-operator] "Unary operator `+` is not supported for type `NoDunder`" +-b # error: [unsupported-operator] "Unary operator `-` is not supported for type `NoDunder`" +~b # error: [unsupported-operator] "Unary operator `~` is not supported for type `NoDunder`" ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md index e01796a9f7..e0cb63d2b5 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/not.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/not.md @@ -187,7 +187,7 @@ class MethodBoolInvalid: def __bool__(self) -> int: return 0 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MethodBoolInvalid`" # revealed: bool reveal_type(not MethodBoolInvalid()) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1ecb4504b6..54beaf3037 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -11574,7 +11574,7 @@ impl<'db> BoolError<'db> { not_boolable_type, .. } => { let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`", + "Boolean conversion is not supported for type `{}`", not_boolable_type.display(context.db()) )); let mut sub = SubDiagnostic::new( @@ -11599,7 +11599,7 @@ impl<'db> BoolError<'db> { return_type, } => { let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{not_boolable}`", + "Boolean conversion is not supported for type `{not_boolable}`", not_boolable = not_boolable_type.display(context.db()), )); let mut sub = SubDiagnostic::new( @@ -11625,7 +11625,7 @@ impl<'db> BoolError<'db> { } Self::NotCallable { not_boolable_type } => { let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`", + "Boolean conversion is not supported for type `{}`", not_boolable_type.display(context.db()) )); let sub = SubDiagnostic::new( @@ -11648,7 +11648,7 @@ impl<'db> BoolError<'db> { .unwrap(); builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for union `{}` \ + "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()), @@ -11657,7 +11657,7 @@ impl<'db> BoolError<'db> { Self::Other { not_boolable_type } => { builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`; \ + "Boolean conversion is not supported for type `{}`; \ it incorrectly implements `__bool__`", not_boolable_type.display(context.db()) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 94366f8cb3..7a26e78bbd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5910,7 +5910,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; }; builder.into_diagnostic(format_args!( - "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", + "Operator `{op}=` is not supported between objects of type `{}` and `{}`", target_type.display(db), value_type.display(db) )); @@ -9679,7 +9679,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) { builder.into_diagnostic(format_args!( - "Unary operator `{op}` is unsupported for type `{}`", + "Unary operator `{op}` is not supported for type `{}`", operand_type.display(self.db()), )); } @@ -9716,7 +9716,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) { let mut diag = builder.into_diagnostic(format_args!( - "Operator `{op}` is unsupported between objects of type `{}` and `{}`", + "Operator `{op}` is not supported between objects of type `{}` and `{}`", left_ty.display(db), right_ty.display(db) )); From c548ef20278f18ec9e88796fd0a1c4b5b5395a40 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 11 Dec 2025 11:25:37 -0500 Subject: [PATCH 28/36] [ty] Squash false positive logs for failing to find `builtins` as a real module I recently started noticing this showing up in the logs for every scope based completion request: ``` 2025-12-11 11:25:35.704329935 DEBUG request{id=29 method="textDocument/completion"}:map_stub_definition: Module `builtins` not found while looking in parent dirs ``` And in particular, it was repeated several times. This was confusing to me because, well, of course `builtins` should resolve. This particular code path comes from looking for the docstrings of completion items. This involves a spelunking that ultimately tries to resolve a "real" module if the stub doesn't have available docstrings. But I guess there is no "real" `builtins` module, so `resolve_real_module` fails. Which is fine, but the noisy logs were annoying since this is an expected case. So here, we carve out a short circuit for `builtins` and also improve the log message. --- .../ty_python_semantic/src/module_resolver/resolver.rs | 9 ++++++++- crates/ty_python_semantic/src/types/ide_support.rs | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index 123b4ac31e..60b565a564 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -265,7 +265,14 @@ fn desperately_resolve_module<'db>( let _span = tracing::trace_span!("desperately_resolve_module", %name).entered(); let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode) else { - tracing::debug!("Module `{name}` not found while looking in parent dirs"); + let extra = match module_name.mode(db) { + ModuleResolveMode::StubsAllowed => "neither stub nor real module file", + ModuleResolveMode::StubsNotAllowed => "stubs not allowed", + ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => { + "stubs not allowed but some shadowing allowed" + } + }; + tracing::debug!("Module `{name}` not found while looking in parent dirs ({extra})"); return None; }; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 5162e8a13d..398084a113 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -843,6 +843,7 @@ mod resolve_definition { use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPathBuf; use ruff_python_ast as ast; + use ruff_python_stdlib::sys::is_builtin_module; use rustc_hash::FxHashSet; use tracing::trace; @@ -1160,6 +1161,14 @@ mod resolve_definition { // here because there isn't really an importing file. However this `resolve_real_module` // can be understood as essentially `import .`, which is also what `file_to_module` is, // so this is in fact exactly the file we want to consider the importer. + // + // ... unless we have a builtin module. i.e., A module embedded + // 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)) { + 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)); From 4fdb4e8219bf8e7b8001121dd61a7c3678200bbf Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 11 Dec 2025 09:53:43 -0800 Subject: [PATCH 29/36] [ty] avoid unions of generic aliases of the same class in fixpoint (#21909) Partially addresses https://github.com/astral-sh/ty/issues/1732 Fixes https://github.com/astral-sh/ty/issues/1800 ## Summary At each fixpoint iteration, we union the "previous" and "current" iteration types, to ensure that the type can only widen at each iteration. This prevents oscillation and ensures convergence. But some unions triggered by this behavior (in particular, unions of differently-specialized generic-aliases of the same class) never simplify, and cause spurious errors. Since we haven't seen examples of oscillating types involving class-literal or generic-alias types, just don't union those. There may be more thorough/principled ways to avoid undesirable unions in fixpoint iteration, but this narrow change seems like it results in strict improvement. ## Test Plan Removes two false positive `unsupported-class-base` in mdtests, and several in the ecosystem, without causing other regression. --- .../resources/mdtest/generics/legacy/classes.md | 5 ----- crates/ty_python_semantic/src/types.rs | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 7 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 bc6ccdb7c1..30d6a89ec0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -860,9 +860,6 @@ reveal_type(Sub) # revealed: U = TypeVar("U") class Base2(Generic[T, U]): ... - -# TODO: no error -# error: [unsupported-base] "Unsupported class base with type ` | `" class Sub2(Base2["Sub2", U]): ... ``` @@ -888,8 +885,6 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") -# TODO: no error "Unsupported class base with type ` | `" -# error: [unsupported-base] class Derived(list[Derived[T]], Generic[T]): ... ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 54beaf3037..e2050ec45f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -912,8 +912,19 @@ impl<'db> Type<'db> { previous: Self, cycle: &salsa::Cycle, ) -> Self { - UnionType::from_elements_cycle_recovery(db, [self, previous]) - .recursive_type_normalized(db, cycle) + // Avoid unioning two generic aliases of the same class together; this union will never + // simplify and is likely to cause downstream problems. This introduces the theoretical + // possibility of cycle oscillation involving such types (because we are not strictly + // widening the type on each iteration), but so far we have not seen an example of that. + match (previous, self) { + (Type::GenericAlias(prev_alias), Type::GenericAlias(curr_alias)) + if prev_alias.origin(db) == curr_alias.origin(db) => + { + self + } + _ => UnionType::from_elements_cycle_recovery(db, [self, previous]), + } + .recursive_type_normalized(db, cycle) } fn is_none(&self, db: &'db dyn Db) -> bool { From fbeeb050af15a96cec59d5e6d7e9a166579b3433 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 11 Dec 2025 18:55:32 +0100 Subject: [PATCH 30/36] [ty] Don't show hover for expressions with no inferred type (#21924) --- crates/ty_ide/src/goto.rs | 16 ++++----- crates/ty_ide/src/hover.rs | 14 ++++++++ crates/ty_ide/src/inlay_hints.rs | 10 +++--- crates/ty_ide/src/semantic_tokens.rs | 18 +++++----- .../ty_python_semantic/src/semantic_model.rs | 35 ++++++++++--------- crates/ty_python_semantic/src/types.rs | 2 +- .../src/types/ide_support.rs | 32 +++++++++++------ 7 files changed, 78 insertions(+), 49 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 217f8b420b..e0f298024d 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -295,7 +295,7 @@ impl<'db> Definitions<'db> { impl GotoTarget<'_> { pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { - let ty = match self { + match self { GotoTarget::Expression(expression) => expression.inferred_type(model), GotoTarget::FunctionDef(function) => function.inferred_type(model), GotoTarget::ClassDef(class) => class.inferred_type(model), @@ -317,7 +317,7 @@ impl GotoTarget<'_> { } => { // We don't currently support hovering the bare `.` so there is always a name let module = import_name(module_name, *component_index); - model.resolve_module_type(Some(module), *level)? + model.resolve_module_type(Some(module), *level) } GotoTarget::StringAnnotationSubexpr { string_expr, @@ -334,16 +334,16 @@ impl GotoTarget<'_> { } else { // TODO: force the typechecker to tell us its secrets // (it computes but then immediately discards these types) - return None; + None } } GotoTarget::BinOp { expression, .. } => { let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?; - ty + Some(ty) } GotoTarget::UnaryOp { expression, .. } => { let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?; - ty + Some(ty) } // TODO: Support identifier targets GotoTarget::PatternMatchRest(_) @@ -353,10 +353,8 @@ impl GotoTarget<'_> { | GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } - | GotoTarget::Globals { .. } => return None, - }; - - Some(ty) + | GotoTarget::Globals { .. } => None, + } } /// Try to get a simplified display of this callable type by resolving overloads diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 8f9add508a..8430a46edc 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -3610,6 +3610,20 @@ def function(): "); } + #[test] + fn hover_tuple_assignment_target() { + let test = CursorTest::builder() + .source( + "test.py", + r#" + (x, y) = "test", 10 + "#, + ) + .build(); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + impl CursorTest { fn hover(&self) -> String { use std::fmt::Write; diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index f3dd178786..d0742f58ec 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -362,8 +362,9 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> { Expr::Name(name) => { if let Some(rhs) = self.assignment_rhs { if name.ctx.is_store() { - let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + if let Some(ty) = expr.inferred_type(&self.model) { + self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + } } } source_order::walk_expr(self, expr); @@ -371,8 +372,9 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> { Expr::Attribute(attribute) => { if let Some(rhs) = self.assignment_rhs { if attribute.ctx.is_store() { - let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + if let Some(ty) = expr.inferred_type(&self.model) { + self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + } } } source_order::walk_expr(self, expr); diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 5667d1506f..79b37265aa 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -273,7 +273,7 @@ impl<'db> SemanticTokenVisitor<'db> { } // Fall back to type-based classification. - let ty = name.inferred_type(self.model); + let ty = name.inferred_type(self.model).unwrap_or(Type::unknown()); let name_str = name.id.as_str(); self.classify_from_type_and_name_str(ty, name_str) } @@ -302,7 +302,9 @@ impl<'db> SemanticTokenVisitor<'db> { let parsed = parsed_module(db, definition.file(db)); let ty = parameter.node(&parsed.load(db)).inferred_type(&model); - if let Type::TypeVar(type_var) = ty { + if let Some(ty) = ty + && let Type::TypeVar(type_var) = ty + { match type_var.typevar(db).kind(db) { TypeVarKind::TypingSelf => { return Some((SemanticTokenType::SelfParameter, modifiers)); @@ -344,9 +346,9 @@ impl<'db> SemanticTokenVisitor<'db> { _ => None, }; - if let Some(value) = value { - let value_ty = value.inferred_type(&model); - + if let Some(value) = value + && let Some(value_ty) = value.inferred_type(&model) + { if value_ty.is_class_literal() || value_ty.is_subclass_of() || value_ty.is_generic_alias() @@ -710,12 +712,12 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { for alias in &import.names { if let Some(asname) = &alias.asname { // For aliased imports (from X import Y as Z), classify Z based on what Y is - let ty = alias.inferred_type(self.model); + let ty = alias.inferred_type(self.model).unwrap_or(Type::unknown()); let (token_type, modifiers) = self.classify_from_alias_type(ty, asname); self.add_token(asname, token_type, modifiers); } else { // For direct imports (from X import Y), use semantic classification - let ty = alias.inferred_type(self.model); + let ty = alias.inferred_type(self.model).unwrap_or(Type::unknown()); let (token_type, modifiers) = self.classify_from_alias_type(ty, &alias.name); self.add_token(&alias.name, token_type, modifiers); @@ -835,7 +837,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { self.visit_expr(&attr.value); // Then add token for the attribute name (e.g., 'path' in 'os.path') - let ty = expr.inferred_type(self.model); + let ty = expr.inferred_type(self.model).unwrap_or(Type::unknown()); let (token_type, modifiers) = Self::classify_from_type_for_attribute(ty, &attr.attr); self.add_token(&attr.attr, token_type, modifiers); diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 54ca0ba74f..049a0e6092 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -196,7 +196,10 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `object.` context. pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec> { - let ty = node.value.inferred_type(self); + let Some(ty) = node.value.inferred_type(self) else { + return Vec::new(); + }; + all_members(self.db, ty) .into_iter() .map(|member| Completion { @@ -400,7 +403,7 @@ pub trait HasType { /// /// ## Panics /// May panic if `self` is from another file than `model`. - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>; + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option>; } pub trait HasDefinition { @@ -412,18 +415,16 @@ pub trait HasDefinition { } impl HasType for ast::ExprRef<'_> { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let index = semantic_index(model.db, model.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 Some(file_scope) = index.try_expression_scope_id(&model.expr_ref_in_ast(*self)) else { - return Type::unknown(); - }; + 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); - infer_scope_types(model.db, scope).expression_type(*self) + infer_scope_types(model.db, scope).try_expression_type(*self) } } @@ -431,7 +432,7 @@ macro_rules! impl_expression_has_type { ($ty: ty) => { impl HasType for $ty { #[inline] - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let expression_ref = ExprRef::from(self); expression_ref.inferred_type(model) } @@ -474,7 +475,7 @@ impl_expression_has_type!(ast::ExprSlice); impl_expression_has_type!(ast::ExprIpyEscapeCommand); impl HasType for ast::Expr { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { match self { Expr::BoolOp(inner) => inner.inferred_type(model), Expr::Named(inner) => inner.inferred_type(model), @@ -525,9 +526,9 @@ macro_rules! impl_binding_has_ty_def { impl HasType for $ty { #[inline] - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let binding = HasDefinition::definition(self, model); - binding_type(model.db, binding) + Some(binding_type(model.db, binding)) } } }; @@ -541,12 +542,12 @@ impl_binding_has_ty_def!(ast::ExceptHandlerExceptHandler); impl_binding_has_ty_def!(ast::TypeParamTypeVar); impl HasType for ast::Alias { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { if &self.name == "*" { - return Type::Never; + return Some(Type::Never); } let index = semantic_index(model.db, model.file); - binding_type(model.db, index.expect_single_definition(self)) + Some(binding_type(model.db, index.expect_single_definition(self))) } } @@ -584,7 +585,7 @@ mod tests { let function = ast.suite()[0].as_function_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); - let ty = function.inferred_type(&model); + let ty = function.inferred_type(&model).unwrap(); assert!(ty.is_function_literal()); @@ -603,7 +604,7 @@ mod tests { let class = ast.suite()[0].as_class_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); - let ty = class.inferred_type(&model); + let ty = class.inferred_type(&model).unwrap(); assert!(ty.is_class_literal()); @@ -624,7 +625,7 @@ mod tests { let import = ast.suite()[0].as_import_from_stmt().unwrap(); let alias = &import.names[0]; let model = SemanticModel::new(&db, bar); - let ty = alias.inferred_type(&model); + let ty = alias.inferred_type(&model).unwrap(); assert!(ty.is_class_literal()); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e2050ec45f..f531fb604e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -878,7 +878,7 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Any) } - pub(crate) const fn unknown() -> Self { + pub const fn unknown() -> Self { Self::Dynamic(DynamicType::Unknown) } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 398084a113..4087d125c6 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -155,7 +155,8 @@ pub fn definitions_for_name<'db>( // https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex if matches!(name_str, "float" | "complex") && let Some(expr) = node.expr_name() - && let Some(union) = expr.inferred_type(&SemanticModel::new(db, file)).as_union() + && let Some(ty) = expr.inferred_type(model) + && let Some(union) = ty.as_union() && is_float_or_complex_annotation(db, union, name_str) { return union @@ -234,7 +235,10 @@ pub fn definitions_for_attribute<'db>( let mut resolved = Vec::new(); // Determine the type of the LHS - let lhs_ty = attribute.value.inferred_type(model); + let Some(lhs_ty) = attribute.value.inferred_type(model) else { + return resolved; + }; + let tys = match lhs_ty { Type::Union(union) => union.elements(model.db()).to_vec(), _ => vec![lhs_ty], @@ -374,7 +378,9 @@ pub fn definitions_for_keyword_argument<'db>( call_expr: &ast::ExprCall, ) -> Vec> { let db = model.db(); - let func_type = call_expr.func.inferred_type(model); + let Some(func_type) = call_expr.func.inferred_type(model) else { + return Vec::new(); + }; let Some(keyword_name) = keyword.arg.as_ref() else { return Vec::new(); @@ -498,7 +504,9 @@ pub fn call_signature_details<'db>( model: &SemanticModel<'db>, call_expr: &ast::ExprCall, ) -> Vec> { - let func_type = call_expr.func.inferred_type(model); + let Some(func_type) = call_expr.func.inferred_type(model) else { + return Vec::new(); + }; // Use into_callable to handle all the complex type conversions if let Some(callable_type) = func_type @@ -507,7 +515,9 @@ pub fn call_signature_details<'db>( { let call_arguments = CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| { - splatted_value.inferred_type(model) + splatted_value + .inferred_type(model) + .unwrap_or(Type::unknown()) }); let bindings = callable_type .bindings(model.db()) @@ -564,7 +574,7 @@ pub fn call_type_simplified_by_overloads( call_expr: &ast::ExprCall, ) -> Option { let db = model.db(); - let func_type = call_expr.func.inferred_type(model); + let func_type = call_expr.func.inferred_type(model)?; // Use into_callable to handle all the complex type conversions let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); @@ -579,7 +589,9 @@ pub fn call_type_simplified_by_overloads( // Hand the overload resolution system as much type info as we have let args = CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| { - splatted_value.inferred_type(model) + splatted_value + .inferred_type(model) + .unwrap_or(Type::unknown()) }); // Try to resolve overloads with the arguments/types we have @@ -612,8 +624,8 @@ pub fn definitions_for_bin_op<'db>( model: &SemanticModel<'db>, binary_op: &ast::ExprBinOp, ) -> Option<(Vec>, Type<'db>)> { - let left_ty = binary_op.left.inferred_type(model); - let right_ty = binary_op.right.inferred_type(model); + 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 { return None; @@ -639,7 +651,7 @@ pub fn definitions_for_unary_op<'db>( model: &SemanticModel<'db>, unary_op: &ast::ExprUnaryOp, ) -> Option<(Vec>, Type<'db>)> { - let operand_ty = unary_op.operand.inferred_type(model); + let operand_ty = unary_op.operand.inferred_type(model)?; let unary_dunder_method = match unary_op.op { ast::UnaryOp::Invert => "__invert__", From c9fe4e2703365b868a3b08364bf9673cef36f13f Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 11 Dec 2025 19:03:52 +0100 Subject: [PATCH 31/36] [ty] Attach salsa db when running ide tests for easier debugging (#21917) --- crates/ty_ide/src/goto_declaration.rs | 5 +++-- crates/ty_ide/src/goto_definition.rs | 5 +++-- crates/ty_ide/src/goto_type_definition.rs | 4 ++-- crates/ty_ide/src/rename.rs | 14 ++++++++------ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 114d43e3b8..2e390711b7 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -2785,8 +2785,9 @@ def ab(a: int, *, c: int): ... impl CursorTest { fn goto_declaration(&self) -> String { - let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset) - else { + let Some(targets) = salsa::attach(&self.db, || { + goto_declaration(&self.db, 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 f37a107c46..e942e8040d 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1697,8 +1697,9 @@ TracebackType impl CursorTest { fn goto_definition(&self) -> String { - let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset) - else { + let Some(targets) = salsa::attach(&self.db, || { + goto_definition(&self.db, 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 16e6165c86..3d7f25f81f 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1900,9 +1900,9 @@ def function(): impl CursorTest { fn goto_type_definition(&self) -> String { - let Some(targets) = + let Some(targets) = salsa::attach(&self.db, || { goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) - else { + }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index fe51f06615..ecea0ffc36 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -98,7 +98,9 @@ mod tests { impl CursorTest { fn prepare_rename(&self) -> String { - let Some(range) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else { + let Some(range) = salsa::attach(&self.db, || { + can_rename(&self.db, self.cursor.file, self.cursor.offset) + }) else { return "Cannot rename".to_string(); }; @@ -106,13 +108,13 @@ mod tests { } fn rename(&self, new_name: &str) -> String { - let Some(_) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else { - return "Cannot rename".to_string(); - }; + let rename_results = salsa::attach(&self.db, || { + can_rename(&self.db, self.cursor.file, self.cursor.offset)?; - let Some(rename_results) = rename(&self.db, self.cursor.file, self.cursor.offset, new_name) - else { + }); + + let Some(rename_results) = rename_results else { return "Cannot rename".to_string(); }; From 34f7a04ef7ecb620658038c1db62a4204eb85388 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 11 Dec 2025 19:04:57 +0100 Subject: [PATCH 32/36] [ty] Handle `Definition`s in `SemanticModel::scope` (#21919) --- .../ty_python_semantic/src/semantic_model.rs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 049a0e6092..fb3895a0e0 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -239,11 +239,45 @@ impl<'db> SemanticModel<'db> { completions } - /// Get the scope of the given node (handles string annotations) + /// 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); match self.node_in_ast(node) { ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), + + // Nodes implementing `HasDefinition` + ast::AnyNodeRef::StmtFunctionDef(function) => Some( + function + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::StmtClassDef(class) => { + Some(class.definition(self).scope(self.db).file_scope_id(self.db)) + } + ast::AnyNodeRef::Parameter(parameter) => Some( + parameter + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::ParameterWithDefault(parameter) => Some( + parameter + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::ExceptHandlerExceptHandler(handler) => Some( + handler + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::TypeParamTypeVar(var) => { + Some(var.definition(self).scope(self.db).file_scope_id(self.db)) + } + + // Fallback node => match node.as_expr_ref() { // If we couldn't identify a specific // expression that we're in, then just From 7a578ce8334d4a1e75f91b1c600fa035f0469bed Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 11 Dec 2025 11:04:28 -0800 Subject: [PATCH 33/36] Ignore ruff:isort like ruff:noqa in new suppressions (#21922) ## Summary Ignores `#ruff:isort` when parsing suppressions similar to `#ruff:noqa`. Should clear up ecosystem issues in #21908 ## Test Plan cargo tests --- crates/ruff_linter/src/suppression.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 9eb12d1026..18c6ad50ce 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -490,8 +490,10 @@ impl<'src> SuppressionParser<'src> { } else if self.cursor.as_str().starts_with("enable") { self.cursor.skip_bytes("enable".len()); Ok(SuppressionAction::Enable) - } else if self.cursor.as_str().starts_with("noqa") { - // file-level "noqa" variant, ignore for now + } else if self.cursor.as_str().starts_with("noqa") + || self.cursor.as_str().starts_with("isort") + { + // alternate suppression variants, ignore for now self.error(ParseErrorKind::NotASuppression) } else { self.error(ParseErrorKind::UnknownAction) From c055d665ef4bbf4002a8c07bdf9b9e76c065a26b Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 11 Dec 2025 11:16:36 -0800 Subject: [PATCH 34/36] Document range suppressions, reorganize suppression docs (#21884) - **Reorganize suppression documentation, document range suppressions** - **Note preview mode requirement** Issue #21874, #3711 --- docs/linter.md | 141 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 28 deletions(-) diff --git a/docs/linter.md b/docs/linter.md index 1f3e4f22b5..6644d54f34 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -279,10 +279,22 @@ Conversely, the following configuration would only enable fixes for `F401`: Ruff supports several mechanisms for suppressing lint errors, be they false positives or permissible violations. -To omit a lint rule entirely, add it to the "ignore" list via the [`lint.ignore`](settings.md#lint_ignore) +### Configuration + +To omit a lint rule everywhere, add it to the "ignore" list via the [`lint.ignore`](settings.md#lint_ignore) setting, either on the command-line or in your `pyproject.toml` or `ruff.toml` file. -To suppress a violation inline, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html). +To omit a lint rule within specific files based on file path prefixes or patterns, +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. + +#### Line-level + +Ruff supports a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html). To ignore an individual violation, add `# noqa: {code}` to the end of the line, like so: ```python @@ -314,6 +326,84 @@ import os # noqa: I001 import abc ``` +The full inline comment specification is as follows: + +- An inline blanket `noqa` comment is given by a case-insensitive match for + `#noqa` with optional whitespace after the `#` symbol, followed by either: the + end of the comment, the beginning of a new comment (`#`), or whitespace + followed by any character other than `:`. +- An inline rule suppression is given by first finding a case-insensitive match + for `#noqa` with optional whitespace after the `#` symbol, optional whitespace + after `noqa`, and followed by the symbol `:`. After this we are expected to + have a list of rule codes which is given by sequences of uppercase ASCII + characters followed by ASCII digits, separated by whitespace or commas. The + list ends at the last valid code. We will attempt to interpret rules with a + missing delimiter (e.g. `F401F841`), though a warning will be emitted in this + case. + +#### Block-level + +*Range suppressions are currently only available in [preview mode](preview.md#preview).* + +To ignore one or more violations within a range or block of code, a "disable" comment +followed by a matching "enable" comment can be used, like so: + +```python +# ruff: disable[E501] +VALUE_1 = "Lorem ipsum dolor sit amet ..." +VALUE_2 = "Lorem ipsum dolor sit amet ..." +VALUE_3 = "Lorem ipsum dolor sit amet ..." +# ruff: enable[E501] +``` + +To define a range, both the "disable" and "enable" comments must have matching codes, +in the same order, as well as matching indentation levels within a logical block of code: + +```python +def foo(): + # ruff: disable[E741, F841] + i = 1 + # ruff: enable[E741, F841] +``` + +If no matching "enable" comment is found, Ruff will also treat this as an "implicit" range. +The implicit range is defined from the starting "disable" comment, until reaching +a logical scope indented less than the starting comment: + +```python +def foo(): + # ruff: disable[E741, F841] + i = 1 + if True: + O = 1 + l = 1 + +# implicit end of range +foo() +``` + +It is strongly suggested to use explicit range suppressions, in order to prevent +accidental suppressions of violations, especially at global module scope. + +Range suppressions cannot be used to enable or select rules that aren't already +selected by the project configuration or runtime flags. An "enable" comment can only +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. + +The full range suppression comment specification is as follows: + +- An own-line comment starting with case sensitive `#ruff:`, with optional whitespace + after the `#` symbol and `:` symbol, followed by either `disable` or `enable` + to start or end a range respectively, immediately followed by `[`, any codes to + be suppressed, and ending with `]`. +- Codes to be suppressed must be separated by commas, with optional whitespace + before or after each code, and may be followed by an optional trailing comma + after the last code. + +#### File-level + To ignore all violations across an entire file, add the line `# ruff: noqa` anywhere in the file, preferably towards the top, like so: @@ -328,51 +418,46 @@ file, preferably towards the top, like so: # ruff: noqa: F841 ``` -Or see the [`lint.per-file-ignores`](settings.md#lint_per-file-ignores) setting, which enables the same -functionality from within your `pyproject.toml` or `ruff.toml` file. - Global `noqa` comments must be on their own line to disambiguate from comments which ignore violations on a single line. Note that Ruff will also respect Flake8's `# flake8: noqa` directive, and will treat it as equivalent to `# ruff: noqa`. -### Full suppression comment specification +The file-level suppression comment specification is as follows: -The full specification is as follows: - -- An inline blanket `noqa` comment is given by a case-insensitive match for - `#noqa` with optional whitespace after the `#` symbol, followed by either: the - end of the comment, the beginning of a new comment (`#`), or whitespace - followed by any character other than `:`. -- An inline rule suppression is given by first finding a case-insensitive match - for `#noqa` with optional whitespace after the `#` symbol, optional whitespace - after `noqa`, and followed by the symbol `:`. After this we are expected to - have a list of rule codes which is given by sequences of uppercase ASCII - characters followed by ASCII digits, separated by whitespace or commas. The - list ends at the last valid code. We will attempt to interpret rules with a - missing delimiter (e.g. `F401F841`), though a warning will be emitted in this - case. - A file-level exemption comment is given by a case-sensitive match for `#ruff:` or `#flake8:`, with optional whitespace after `#` and before `:`, followed by optional whitespace and a case-insensitive match for `noqa`. After this, the - specification is as in the inline case. + specification is as in the inline `noqa` suppressions above. -### Detecting unused suppression comments +### Detecting unused suppressions Ruff implements a special rule, [`unused-noqa`](https://docs.astral.sh/ruff/rules/unused-noqa/), -under the `RUF100` code, to enforce that your `noqa` directives are "valid", in that the violations -they _say_ they ignore are actually being triggered on that line (and thus suppressed). To flag -unused `noqa` directives, run: `ruff check /path/to/file.py --extend-select RUF100`. +under the `RUF100` code, to enforce that your suppressions are "valid", in that the violations +they _say_ they ignore are actually being triggered and suppressed. To flag +unused suppression comments, run Ruff with `--extend-select RUF100`, like so: -Ruff can also _remove_ any unused `noqa` directives via its fix functionality. To remove any -unused `noqa` directives, run: `ruff check /path/to/file.py --extend-select RUF100 --fix`. +```shell-session +$ ruff check /path/to/file.py --extend-select RUF100 +``` + +Ruff can also _remove_ any unused suppression comments via its fix functionality. +To remove any unused suppressions, run Ruff with `--fix`, like so: + +```shell-session +$ ruff check /path/to/file.py --extend-select RUF100 --fix +``` ### Inserting necessary suppression comments Ruff can _automatically add_ `noqa` directives to all lines that contain violations, which is useful when migrating a new codebase to Ruff. To automatically add `noqa` directives to all -relevant lines (with the appropriate rule codes), run: `ruff check /path/to/file.py --add-noqa`. +relevant lines (with the appropriate rule codes), run Ruff with `--add-noqa`, like so: + +```shell-session +$ ruff check /path/to/file.py --add-noqa +``` ### Action comments From d442433e93840edc47f77ab10a85ebfbcb18211b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 11 Dec 2025 20:22:21 +0100 Subject: [PATCH 35/36] [ty] Fix workspace symbols to return members too (#21926) --- crates/ty_ide/src/workspace_symbols.rs | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index a9e5e78820..3224c50baf 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -1,4 +1,4 @@ -use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only}; +use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file}; use ruff_db::files::File; use ty_project::Db; @@ -26,7 +26,7 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { for file in files.iter() { let db = db.dyn_clone(); s.spawn(move |_| { - for (_, symbol) in symbols_for_file_global_only(&*db, *file).search(query) { + for (_, symbol) in symbols_for_file(&*db, *file).search(query) { // It seems like we could do better here than // locking `results` for every single symbol, // but this works pretty well as it is. @@ -64,7 +64,7 @@ mod tests { }; #[test] - fn test_workspace_symbols_multi_file() { + fn workspace_symbols_multi_file() { let test = CursorTest::builder() .source( "utils.py", @@ -126,6 +126,30 @@ API_BASE_URL = 'https://api.example.com' "); } + #[test] + fn members() { + let test = CursorTest::builder() + .source( + "utils.py", + " +class Test: + def from_path(): ... +", + ) + .build(); + + assert_snapshot!(test.workspace_symbols("from"), @r" + info[workspace-symbols]: WorkspaceSymbolInfo + --> utils.py:3:9 + | + 2 | class Test: + 3 | def from_path(): ... + | ^^^^^^^^^ + | + info: Method from_path + "); + } + impl CursorTest { fn workspace_symbols(&self, query: &str) -> String { let symbols = workspace_symbols(&self.db, query); From c8851ecf704c1e989c5b840e2d53ac24210b4aec Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 11 Dec 2025 15:00:18 -0500 Subject: [PATCH 36/36] [ty] Defer all parameter and return type annotations (#21906) As described in astral-sh/ty#1729, we previously had a salsa cycle when inferring the signature of many function definitions. The most obvious case happened when (a) the function was decorated, (b) it had no PEP-695 type params, and (c) annotations were not always deferred (e.g. in a stub file). We currently evaluate and apply function decorators eagerly, as part of `infer_function_definition`. Applying a decorator requires knowing the signature of the function being decorated. There were two places where signature construction called `infer_definition_types` cyclically. The simpler case was that we were looking up the generic context and decorator list of the function to determine whether it has an implicit `self` parameter. Before, we used `infer_definition_types` to determine that information. But since we're in the middle of signature construction for the function, we can just thread the information through directly. The harder case is that signature construction requires knowing the inferred parameter and return type annotations. When (b) and (c) hold, those type annotations are inferred in `infer_function_definition`! (In theory, we've already finished that by the time we start applying decorators, but signature construction doesn't know that.) If annotations are deferred, the params/return annotations are inferred in `infer_deferred_types`; if there are PEP-695 type params, they're inferred in `infer_function_type_params`. Both of those are different salsa queries, and don't induce this cycle. So the quick fix here is to always defer inference of the function params/return, so that they are always inferred under a different salsa query. A more principled fix would be to apply decorators lazily, just like we construct signatures lazily. But that is a more invasive fix. Fixes astral-sh/ty#1729 --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/annotations/self.md | 62 +++- .../mdtest/generics/pep695/paramspec.md | 5 +- .../ty_python_semantic/src/types/context.rs | 4 +- .../ty_python_semantic/src/types/function.rs | 90 +++++- .../src/types/infer/builder.rs | 38 ++- .../src/types/signatures.rs | 286 ++++++------------ 6 files changed, 269 insertions(+), 216 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 7fb465fdbd..016cc848b8 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -282,8 +282,10 @@ reveal_type(C().method()) # revealed: C ## Class Methods +### Explicit + ```py -from typing import Self, TypeVar +from typing import Self class Shape: def foo(self: Self) -> Self: @@ -298,6 +300,64 @@ class Circle(Shape): ... reveal_type(Shape().foo()) # revealed: Shape reveal_type(Shape.bar()) # revealed: Shape + +reveal_type(Circle().foo()) # revealed: Circle +reveal_type(Circle.bar()) # revealed: Circle +``` + +### Implicit + +```py +from typing import Self + +class Shape: + def foo(self) -> Self: + return self + + @classmethod + def bar(cls) -> Self: + reveal_type(cls) # revealed: type[Self@bar] + return cls() + +class Circle(Shape): ... + +reveal_type(Shape().foo()) # revealed: Shape +reveal_type(Shape.bar()) # revealed: Shape + +reveal_type(Circle().foo()) # revealed: Circle +reveal_type(Circle.bar()) # revealed: Circle +``` + +### Implicit in generic class + +```py +from typing import Self + +class GenericShape[T]: + def foo(self) -> Self: + return self + + @classmethod + def bar(cls) -> Self: + reveal_type(cls) # revealed: type[Self@bar] + return cls() + + @classmethod + def baz[U](cls, u: U) -> "GenericShape[U]": + reveal_type(cls) # revealed: type[Self@baz] + return cls() + +class GenericCircle[T](GenericShape[T]): ... + +reveal_type(GenericShape().foo()) # revealed: GenericShape[Unknown] +reveal_type(GenericShape.bar()) # revealed: GenericShape[Unknown] +reveal_type(GenericShape[int].bar()) # revealed: GenericShape[int] +reveal_type(GenericShape.baz(1)) # revealed: GenericShape[Literal[1]] + +reveal_type(GenericCircle().foo()) # revealed: GenericCircle[Unknown] +reveal_type(GenericCircle.bar()) # revealed: GenericCircle[Unknown] +reveal_type(GenericCircle[int].bar()) # revealed: GenericCircle[int] +reveal_type(GenericCircle.baz(1)) # revealed: GenericShape[Literal[1]] ``` ## Attributes 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 75c76d5d02..81c1960de4 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -417,16 +417,13 @@ The `converter` function act as a decorator here: def f3(x: int, y: str) -> int: return 1 -# TODO: This should reveal `(x: int, y: str) -> bool` but there's a cycle: https://github.com/astral-sh/ty/issues/1729 -reveal_type(f3) # revealed: ((x: int, y: str) -> bool) | ((x: Divergent, y: Divergent) -> bool) +reveal_type(f3) # revealed: (x: int, y: str) -> bool reveal_type(f3(1, "a")) # revealed: bool reveal_type(f3(x=1, y="a")) # revealed: bool reveal_type(f3(1, y="a")) # revealed: bool reveal_type(f3(y="a", x=1)) # revealed: bool -# TODO: There should only be one error but the type of `f3` is a union: https://github.com/astral-sh/ty/issues/1729 -# error: [missing-argument] "No argument provided for required parameter `y`" # error: [missing-argument] "No argument provided for required parameter `y`" f3(1) # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["a"]`" diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 7eb84e0d01..662b38b302 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -177,8 +177,8 @@ impl<'db, 'ast> InferContext<'db, 'ast> { std::mem::replace(&mut self.multi_inference, multi_inference) } - pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) { - self.no_type_check = no_type_check; + pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) -> InNoTypeCheck { + std::mem::replace(&mut self.no_type_check, no_type_check) } fn is_in_no_type_check(&self) -> bool { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index cc2c358590..6b61316a69 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -73,7 +73,8 @@ use crate::types::diagnostic::{ report_runtime_check_against_non_runtime_checkable_protocol, }; use crate::types::display::DisplaySettings; -use crate::types::generics::{GenericContext, InferableTypeVars}; +use crate::types::generics::{GenericContext, InferableTypeVars, typing_self}; +use crate::types::infer::nearest_enclosing_class; use crate::types::list_members::all_members; use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; @@ -82,8 +83,9 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, CallableTypeKind, ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, - NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, - UnionBuilder, binding_type, definition_expression_type, walk_signature, + NormalizedVisitor, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, + TypeContext, TypeMapping, TypeRelation, UnionBuilder, binding_type, definition_expression_type, + infer_definition_types, walk_signature, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -499,13 +501,91 @@ impl<'db> OverloadLiteral<'db> { index, ); - Signature::from_function( + let mut raw_signature = Signature::from_function( db, pep695_ctx, definition, function_stmt_node, has_implicitly_positional_first_parameter, - ) + ); + + let generic_context = raw_signature.generic_context; + raw_signature.add_implicit_self_annotation(db, || { + if self.is_staticmethod(db) { + return None; + } + + // We have not yet added an implicit annotation to the `self` parameter, so any + // typevars that currently appear in the method's generic context come from explicit + // annotations. + let method_has_explicit_self = generic_context + .is_some_and(|context| context.variables(db).any(|v| v.typevar(db).is_self(db))); + + let class_scope_id = definition.scope(db); + let class_scope = index.scope(class_scope_id.file_scope_id(db)); + let class_node = class_scope.node().as_class()?; + let class_def = index.expect_single_definition(class_node); + let Type::ClassLiteral(class_literal) = infer_definition_types(db, class_def) + .declaration_type(class_def) + .inner_type() + else { + return None; + }; + let class_is_generic = class_literal.generic_context(db).is_some(); + let class_is_fallback = class_literal + .known(db) + .is_some_and(KnownClass::is_fallback_class); + + // Normally we implicitly annotate `self` or `cls` with `Self` or `type[Self]`, and + // create a `Self` typevar that we then have to solve for whenever this method is + // called. As an optimization, we can skip creating that typevar in certain situations: + // + // - The method cannot use explicit `Self` in any other parameter annotations, + // or in its return type. If it does, then we really do need specialization + // inference at each call site to see which specific instance type should be + // used in those other parameters / return type. + // + // - The class cannot be generic. If it is, then we might need an actual `Self` + // typevar to help carry through constraints that relate the instance type to + // other typevars in the method signature. + // + // - The class cannot be a "fallback class". A fallback class is used like a mixin, + // and so we need specialization inference to determine the "real" class that the + // fallback is augmenting. (See KnownClass::is_fallback_class for more details.) + 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 class = nearest_enclosing_class(db, index, scope_id).unwrap(); + + let typing_self = typing_self(db, scope_id, typevar_binding_context, class).expect( + "We should always find the surrounding class \ + for an implicit self: Self annotation", + ); + + if self.is_classmethod(db) { + Some(SubclassOfType::from( + db, + SubclassOfInner::TypeVar(typing_self), + )) + } else { + Some(Type::TypeVar(typing_self)) + } + } 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) { + Some(SubclassOfType::from( + db, + SubclassOfInner::Class(ClassType::NonGeneric(class_literal)), + )) + } else { + Some(class_literal.to_non_generic_instance(db)) + } + } + }); + + raw_signature } pub(crate) fn parameter_span( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7a26e78bbd..00141d4ca8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2241,7 +2241,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { name, type_params, parameters, - returns, + returns: _, body: _, decorator_list, } = function; @@ -2288,21 +2288,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_expression(default, TypeContext::default()); } - // If there are type params, parameters and returns are evaluated in that scope, that is, in - // `infer_function_type_params`, rather than here. + // If there are type params, parameters and returns are evaluated in that scope. Otherwise, + // we always defer the inference of the parameters and returns. That ensures that we do not + // add any spurious salsa cycles when applying decorators below. (Applying a decorator + // requires getting the signature of this function definition, which in turn requires + // (lazily) inferring the parameter and return types.) if type_params.is_none() { - if self.defer_annotations() { - self.deferred.insert(definition, self.multi_inference_state); - } else { - let previous_typevar_binding_context = - self.typevar_binding_context.replace(definition); - self.infer_return_type_annotation( - returns.as_deref(), - DeferredExpressionState::None, - ); - self.infer_parameters(parameters); - self.typevar_binding_context = previous_typevar_binding_context; - } + self.deferred.insert(definition, self.multi_inference_state); } let known_function = @@ -2946,10 +2938,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, function: &ast::StmtFunctionDef, ) { + let mut prev_in_no_type_check = self.context.set_in_no_type_check(InNoTypeCheck::Yes); + for decorator in &function.decorator_list { + let decorator_type = self.infer_decorator(decorator); + if let Type::FunctionLiteral(function) = decorator_type + && let Some(KnownFunction::NoTypeCheck) = function.known(self.db()) + { + // If the function is decorated with the `no_type_check` decorator, + // we need to suppress any errors that come after the decorators. + prev_in_no_type_check = InNoTypeCheck::Yes; + break; + } + } + self.context.set_in_no_type_check(prev_in_no_type_check); + let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); self.infer_return_type_annotation( function.returns.as_deref(), - DeferredExpressionState::Deferred, + self.defer_annotations().into(), ); self.infer_parameters(function.parameters.as_ref()); self.typevar_binding_context = previous_typevar_binding_context; diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 9f8d7ccacd..45c3f81de2 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -13,107 +13,46 @@ use std::{collections::HashMap, slice::Iter}; use itertools::{EitherOrBoth, Itertools}; -use ruff_db::parsed::parsed_module; -use ruff_python_ast::ParameterWithDefault; use smallvec::{SmallVec, smallvec_inline}; -use super::{ - ClassType, DynamicType, Type, TypeVarVariance, definition_expression_type, - infer_definition_types, semantic_index, -}; -use crate::semantic_index::definition::{Definition, DefinitionKind}; +use super::{DynamicType, Type, TypeVarVariance, definition_expression_type, semantic_index}; +use crate::semantic_index::definition::Definition; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; -use crate::types::function::{is_implicit_classmethod, is_implicit_staticmethod}; -use crate::types::generics::{ - GenericContext, InferableTypeVars, typing_self, walk_generic_context, -}; -use crate::types::infer::nearest_enclosing_class; +use crate::types::generics::{GenericContext, InferableTypeVars, walk_generic_context}; +use crate::types::infer::{infer_deferred_types, infer_scope_types}; use crate::types::{ - ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind, ClassLiteral, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, - KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, SubclassOfInner, - SubclassOfType, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, + KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, + TypeMapping, TypeRelation, VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; -#[derive(Clone, Copy, Debug)] -#[expect(clippy::struct_excessive_bools)] -struct MethodInformation<'db> { - is_staticmethod: bool, - is_classmethod: bool, - method_may_be_generic: bool, - class_literal: ClassLiteral<'db>, - class_is_generic: bool, -} - -fn infer_method_information<'db>( +/// Infer the type of a parameter or return annotation in a function signature. +/// +/// This is very similar to [`definition_expression_type`], but knows that `TypeInferenceBuilder` +/// will always infer the parameters and return of a function in its PEP-695 typevar scope, if +/// there is one; otherwise they will be inferred in the function definition scope, but will always +/// be deferred. (This prevents spurious salsa cycles when we need the signature of the function +/// while in the middle of inferring its definition scope — for instance, when applying +/// decorators.) +fn function_signature_expression_type<'db>( db: &'db dyn Db, definition: Definition<'db>, -) -> Option> { - let DefinitionKind::Function(function_definition) = definition.kind(db) else { - return None; - }; - - let class_scope_id = definition.scope(db); - let file = class_scope_id.file(db); - let module = parsed_module(db, file).load(db); + expression: &ast::Expr, +) -> Type<'db> { + let file = definition.file(db); let index = semantic_index(db, file); - - let class_scope = index.scope(class_scope_id.file_scope_id(db)); - let class_node = class_scope.node().as_class()?; - - let function_node = function_definition.node(&module); - let function_name = &function_node.name; - - let mut is_staticmethod = is_implicit_classmethod(function_name); - let mut is_classmethod = is_implicit_staticmethod(function_name); - - let inference = infer_definition_types(db, definition); - for decorator in &function_node.decorator_list { - let decorator_ty = inference.expression_type(&decorator.expression); - - match decorator_ty - .as_class_literal() - .and_then(|class| class.known(db)) - { - Some(KnownClass::Staticmethod) => { - is_staticmethod = true; - } - Some(KnownClass::Classmethod) => { - is_classmethod = true; - } - _ => {} - } + let file_scope = index.expression_scope_id(expression); + let scope = file_scope.to_scope_id(db, file); + if scope == definition.scope(db) { + // expression is in the function definition scope, but always deferred + infer_deferred_types(db, definition).expression_type(expression) + } else { + // expression is in the PEP-695 type params sub-scope + infer_scope_types(db, scope).expression_type(expression) } - - let method_may_be_generic = match inference.declaration_type(definition).inner_type() { - Type::FunctionLiteral(f) => f.signature(db).overloads.iter().any(|s| { - s.generic_context - .is_some_and(|context| context.variables(db).any(|v| v.typevar(db).is_self(db))) - }), - _ => true, - }; - - let class_def = index.expect_single_definition(class_node); - let (class_literal, class_is_generic) = match infer_definition_types(db, class_def) - .declaration_type(class_def) - .inner_type() - { - Type::ClassLiteral(class_literal) => { - (class_literal, class_literal.generic_context(db).is_some()) - } - Type::GenericAlias(alias) => (alias.origin(db), true), - _ => return None, - }; - - Some(MethodInformation { - is_staticmethod, - is_classmethod, - method_may_be_generic, - class_literal, - class_is_generic, - }) } /// The signature of a single callable. If the callable is overloaded, there is a separate @@ -615,7 +554,7 @@ impl<'db> Signature<'db> { let return_ty = function_node .returns .as_ref() - .map(|returns| definition_expression_type(db, definition, returns.as_ref())); + .map(|returns| function_signature_expression_type(db, definition, returns.as_ref())); let legacy_generic_context = GenericContext::from_function_params(db, definition, ¶meters, return_ty); let full_generic_context = GenericContext::merge_pep695_and_legacy( @@ -771,6 +710,57 @@ impl<'db> Signature<'db> { &self.parameters } + /// Adds an implicit annotation to the first parameter of this signature, if that parameter is + /// positional and does not already have an annotation. We do not check whether that's the + /// right thing to do! The caller must determine whether the first parameter is actually a + /// `self` or `cls` parameter, and must determine the correct type to use as the implicit + /// annotation. + pub(crate) fn add_implicit_self_annotation( + &mut self, + db: &'db dyn Db, + self_type: impl FnOnce() -> Option>, + ) { + if let Some(first_parameter) = self.parameters.value.first_mut() + && first_parameter.is_positional() + && first_parameter.annotated_type.is_none() + && let Some(self_type) = self_type() + { + first_parameter.annotated_type = Some(self_type); + first_parameter.inferred_annotation = true; + + // If we've added an implicit `self` annotation, we might need to update the + // signature's generic context, too. (The generic context should include any synthetic + // typevars created for `typing.Self`, even if the `typing.Self` annotation was added + // implicitly.) + let self_typevar = match self_type { + Type::TypeVar(self_typevar) => Some(self_typevar), + Type::SubclassOf(subclass_of) => subclass_of.into_type_var(), + _ => None, + }; + + if let Some(self_typevar) = self_typevar { + match self.generic_context.as_mut() { + Some(generic_context) + if generic_context + .binds_typevar(db, self_typevar.typevar(db)) + .is_some() => {} + Some(generic_context) => { + *generic_context = GenericContext::from_typevar_instances( + db, + std::iter::once(self_typevar).chain(generic_context.variables(db)), + ); + } + None => { + self.generic_context = Some(GenericContext::from_typevar_instances( + db, + std::iter::once(self_typevar), + )); + } + } + } + } + } + /// Return the definition associated with this signature, if any. pub(crate) fn definition(&self) -> Option> { self.definition @@ -1674,84 +1664,16 @@ impl<'db> Parameters<'db> { }) }; - let method_info = infer_method_information(db, definition); - let is_staticmethod = method_info.is_some_and(|f| f.is_staticmethod); - let is_classmethod = method_info.is_some_and(|f| f.is_classmethod); - - let inferred_annotation = |arg: &ParameterWithDefault| { - if let Some(MethodInformation { - method_may_be_generic, - class_literal, - class_is_generic, - .. - }) = method_info - && !is_staticmethod - && arg.parameter.annotation().is_none() - && parameters.index(arg.name().id()) == Some(0) - { - if method_may_be_generic - || class_is_generic - || class_literal - .known(db) - .is_some_and(KnownClass::is_fallback_class) - { - let scope_id = definition.scope(db); - let typevar_binding_context = Some(definition); - let index = semantic_index(db, scope_id.file(db)); - let class = nearest_enclosing_class(db, index, scope_id).unwrap(); - - let typing_self = typing_self(db, scope_id, typevar_binding_context, class) - .expect("We should always find the surrounding class for an implicit self: Self annotation"); - - if is_classmethod { - Some(SubclassOfType::from( - db, - SubclassOfInner::TypeVar(typing_self), - )) - } else { - Some(Type::TypeVar(typing_self)) - } - } else { - // For methods of non-generic classes that are not otherwise generic (e.g. return `Self` or - // have additional type parameters), the implicit `Self` type of the `self`, or the implicit - // `type[Self]` type of the `cls` parameter, would be the only type variable, so we can just - // use the class directly. - if is_classmethod { - Some(SubclassOfType::from( - db, - SubclassOfInner::Class(ClassType::NonGeneric(class_literal)), - )) - } else { - Some(class_literal.to_non_generic_instance(db)) - } - } - } else { - None - } - }; - let pos_only_param = |param: &ast::ParameterWithDefault| { - if let Some(inferred_annotation_type) = inferred_annotation(param) { - Parameter { - annotated_type: Some(inferred_annotation_type), - inferred_annotation: true, - kind: ParameterKind::PositionalOnly { - name: Some(param.parameter.name.id.clone()), - default_type: default_type(param), - }, - form: ParameterForm::Value, - } - } else { - Parameter::from_node_and_kind( - db, - definition, - ¶m.parameter, - ParameterKind::PositionalOnly { - name: Some(param.parameter.name.id.clone()), - default_type: default_type(param), - }, - ) - } + Parameter::from_node_and_kind( + db, + definition, + ¶m.parameter, + ParameterKind::PositionalOnly { + name: Some(param.parameter.name.id.clone()), + default_type: default_type(param), + }, + ) }; let mut positional_only: Vec = posonlyargs.iter().map(pos_only_param).collect(); @@ -1775,27 +1697,15 @@ impl<'db> Parameters<'db> { } let positional_or_keyword = pos_or_keyword_iter.map(|arg| { - if let Some(inferred_annotation_type) = inferred_annotation(arg) { - Parameter { - annotated_type: Some(inferred_annotation_type), - inferred_annotation: true, - kind: ParameterKind::PositionalOrKeyword { - name: arg.parameter.name.id.clone(), - default_type: default_type(arg), - }, - form: ParameterForm::Value, - } - } else { - Parameter::from_node_and_kind( - db, - definition, - &arg.parameter, - ParameterKind::PositionalOrKeyword { - name: arg.parameter.name.id.clone(), - default_type: default_type(arg), - }, - ) - } + Parameter::from_node_and_kind( + db, + definition, + &arg.parameter, + ParameterKind::PositionalOrKeyword { + name: arg.parameter.name.id.clone(), + default_type: default_type(arg), + }, + ) }); let variadic = vararg.as_ref().map(|arg| { @@ -2212,7 +2122,7 @@ impl<'db> Parameter<'db> { Self { annotated_type: parameter .annotation() - .map(|annotation| definition_expression_type(db, definition, annotation)), + .map(|annotation| function_signature_expression_type(db, definition, annotation)), kind, form: ParameterForm::Value, inferred_annotation: false,