From c7b41060f4aeeb8af0614e24590a508528e09d72 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 13 Jan 2026 14:56:56 +0000 Subject: [PATCH] [ty] Improve disambiguation of types (#22547) --- .../resources/mdtest/attributes.md | 2 +- .../resources/mdtest/call/type.md | 6 +++--- .../mdtest/diagnostics/same_names.md | 2 +- .../resources/mdtest/mro.md | 4 ++-- .../resources/mdtest/public_types.md | 2 +- ...etting_attributes_o…_(467e26496f4c0c13).snap | 2 +- ...gnostic_for_funct…_(340818ba77052e65).snap | 1 + ...__bases__`_includes…_(d2532518c44112c8).snap | 2 +- ...gnostic_when_the_…_(93e8ab913ead83b2).snap | 2 +- .../src/types/class_base.rs | 21 ++++++++++++++++--- .../ty_python_semantic/src/types/display.rs | 8 ++++--- .../ty_python_semantic/src/types/function.rs | 11 +++++++++- crates/ty_test/src/matcher.rs | 8 ++++--- 13 files changed, 50 insertions(+), 21 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 79c2dd6ed8..d12d243aef 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1208,7 +1208,7 @@ def _(flag: bool): reveal_type(C1.y) # revealed: int | str C1.y = 100 - # error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `.C1 @ src/mdtest_snippet.py:3'> | .C1 @ src/mdtest_snippet.py:8'>`" + # error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `.C1 @ src/mdtest_snippet.py:3:15'> | .C1 @ src/mdtest_snippet.py:8:15'>`" C1.y = "problematic" class C2: diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 01a4cb4503..5c35183e44 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -77,9 +77,9 @@ def takes_foo2(x: Foo2) -> None: ... takes_foo1(foo1) # OK takes_foo2(foo2) # OK -# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`" +# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5:8`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6:8`" takes_foo1(foo2) -# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`" +# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6:8`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5:8`" takes_foo2(foo1) ``` @@ -762,7 +762,7 @@ def make_classes(name1: str, name2: str): def inner(x: cls1): ... - # error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.. @ src/mdtest_snippet.py:8`, found `mdtest_snippet.. @ src/mdtest_snippet.py:9`" + # error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.. @ src/mdtest_snippet.py:8:12`, found `mdtest_snippet.. @ src/mdtest_snippet.py:9:12`" inner(cls2()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md index 5414312f9f..92d6e33eb6 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md @@ -86,7 +86,7 @@ class MyClass: ... def get_MyClass() -> MyClass: from . import make_MyClass - # error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1`, found `package.foo.MyClass @ src/package/foo.pyi:1`" + # error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1:7`, found `package.foo.MyClass @ src/package/foo.pyi:1:7`" return make_MyClass() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index eea081f661..f1f38aac30 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -398,10 +398,10 @@ if returns_bool(): else: class B(Y, X): ... -# revealed: (, , , , ) | (, , , , ) +# revealed: (, , , , ) | (, , , , ) reveal_mro(B) -# error: 12 [unsupported-base] "Unsupported class base with type ` | `" +# error: 12 [unsupported-base] "Unsupported class base with type ` | `" class Z(A, B): ... reveal_mro(Z) # revealed: (, Unknown, ) diff --git a/crates/ty_python_semantic/resources/mdtest/public_types.md b/crates/ty_python_semantic/resources/mdtest/public_types.md index 56b6a803c5..f8e522e767 100644 --- a/crates/ty_python_semantic/resources/mdtest/public_types.md +++ b/crates/ty_python_semantic/resources/mdtest/public_types.md @@ -339,7 +339,7 @@ class A: ... def f(x: A): # TODO: no error - # error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12 | mdtest_snippet.A @ src/mdtest_snippet.py:13` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13`" + # error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12:7 | mdtest_snippet.A @ src/mdtest_snippet.py:13:7` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13:7`" x = A() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment…_-_Attribute_assignment_-_Setting_attributes_o…_(467e26496f4c0c13).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment…_-_Attribute_assignment_-_Setting_attributes_o…_(467e26496f4c0c13).snap index 6eed7eb941..f8b21b795d 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment…_-_Attribute_assignment_-_Setting_attributes_o…_(467e26496f4c0c13).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment…_-_Attribute_assignment_-_Setting_attributes_o…_(467e26496f4c0c13).snap @@ -38,7 +38,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as # Diagnostics ``` -error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `.C1 @ src/mdtest_snippet.py:3'> | .C1 @ src/mdtest_snippet.py:7'>` +error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `.C1 @ src/mdtest_snippet.py:3:15'> | .C1 @ src/mdtest_snippet.py:7:15'>` --> src/mdtest_snippet.py:11:5 | 10 | # TODO: The error message here could be improved to explain why the assignment fails. diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Diagnostic_for_funct…_(340818ba77052e65).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Diagnostic_for_funct…_(340818ba77052e65).snap index 6ff1668d26..e5f50123b5 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Diagnostic_for_funct…_(340818ba77052e65).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Diagnostic_for_funct…_(340818ba77052e65).snap @@ -2,6 +2,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- + --- mdtest name: attributes.md - Attributes - Diagnostic for function attribute accessed on `Callable` type mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap index d87f10c1fa..d86a74b2db 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap @@ -67,7 +67,7 @@ warning[unsupported-base]: Unsupported class base 25 | class C: ... 26 | 27 | class D(C): ... # error: [unsupported-base] - | ^ Has type `.C @ src/mdtest_snippet.py:23'> | .C @ src/mdtest_snippet.py:25'>` + | ^ Has type `.C @ src/mdtest_snippet.py:23:15'> | .C @ src/mdtest_snippet.py:25:15'>` | info: ty cannot resolve a consistent method resolution order (MRO) for class `D` due to this base info: Only class objects or `Any` are supported as class bases diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_…_(93e8ab913ead83b2).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_…_(93e8ab913ead83b2).snap index 9df110ca13..b5dc280c54 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_…_(93e8ab913ead83b2).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_…_(93e8ab913ead83b2).snap @@ -33,7 +33,7 @@ error[invalid-super-argument]: Argument is not a valid class 7 | else: 8 | class A: ... 9 | super(A, A()) # error: [invalid-super-argument] - | ^^^^^^^^^^^^^ Argument has type `.A @ src/mdtest_snippet.py:6'> | .A @ src/mdtest_snippet.py:8'>` + | ^^^^^^^^^^^^^ Argument has type `.A @ src/mdtest_snippet.py:6:15'> | .A @ src/mdtest_snippet.py:8:15'>` | info: rule `invalid-super-argument` is enabled by default diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index a6c428a58e..e1b2daa1b1 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,7 +1,7 @@ -use crate::Db; use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; +use crate::{Db, DisplaySettings}; use crate::types::tuple::TupleType; use crate::types::{ @@ -408,16 +408,27 @@ impl<'db> ClassBase<'db> { } pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { + self.display_with(db, DisplaySettings::default()) + } + + pub(super) fn display_with( + self, + db: &'db dyn Db, + display_settings: DisplaySettings<'db>, + ) -> impl std::fmt::Display { struct ClassBaseDisplay<'db> { db: &'db dyn Db, base: ClassBase<'db>, + settings: DisplaySettings<'db>, } impl std::fmt::Display for ClassBaseDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.base { ClassBase::Dynamic(dynamic) => dynamic.fmt(f), - ClassBase::Class(class) => Type::from(class).display(self.db).fmt(f), + ClassBase::Class(class) => Type::from(class) + .display_with(self.db, self.settings.clone()) + .fmt(f), ClassBase::Protocol => f.write_str("typing.Protocol"), ClassBase::Generic => f.write_str("typing.Generic"), ClassBase::TypedDict => f.write_str("typing.TypedDict"), @@ -425,7 +436,11 @@ impl<'db> ClassBase<'db> { } } - ClassBaseDisplay { db, base: self } + ClassBaseDisplay { + db, + base: self, + settings: display_settings, + } } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index dd2811aab2..1cae0b11d7 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -7,9 +7,10 @@ use std::fmt::{self, Display, Formatter, Write}; use std::rc::Rc; use ruff_db::files::FilePath; -use ruff_db::source::line_index; +use ruff_db::source::{line_index, source_text}; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; +use ruff_source_file::LineColumn; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -581,9 +582,10 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> { FilePath::Vendored(_) | FilePath::SystemVirtual(_) => Cow::Borrowed(path), }; let line_index = line_index(self.db, file); - let line_number = line_index.line_index(class_offset); + let LineColumn { line, column } = + line_index.line_column(class_offset, &source_text(self.db, file)); f.set_invalid_type_annotation(); - write!(f, " @ {path}:{line_number}")?; + write!(f, " @ {path}:{line}:{column}")?; } Ok(()) } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 48a6b13e8f..18aa4815de 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1818,10 +1818,19 @@ impl KnownFunction { let mut diag = builder.into_diagnostic("Revealed MRO"); let span = context.span(&call_expression.arguments.args[0]); let mut message = String::new(); + let display_settings = DisplaySettings::from_possibly_ambiguous_types( + db, + classes + .iter() + .flat_map(|class| class.iter_mro(db)) + .filter_map(ClassBase::into_class), + ); for (i, class) in classes.iter().enumerate() { message.push('('); for class in class.iter_mro(db) { - message.push_str(&class.display(db).to_string()); + message.push_str( + &class.display_with(db, display_settings.clone()).to_string(), + ); // Omit the comma for the last element (which is always `object`) if class .into_class() diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs index 5e67f5f778..06694fce55 100644 --- a/crates/ty_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -231,11 +231,11 @@ fn discard_todo_metadata(ty: &str) -> Cow<'_, str> { /// Normalize paths in diagnostics to Unix paths before comparing them against /// the expected type. Doing otherwise means that it's hard to write cross-platform /// tests, since in some edge cases the display of a type can include a path to the -/// file in which the type was defined (e.g. `foo.bar.A @ src/foo/bar.py:10` on Unix, -/// but `foo.bar.A @ src\foo\bar.py:10` on Windows). +/// file in which the type was defined (e.g. `foo.bar.A @ src/foo/bar.py:10:5` on Unix, +/// but `foo.bar.A @ src\foo\bar.py:10:5` on Windows). fn normalize_paths(ty: &str) -> Cow<'_, str> { static PATH_IN_CLASS_DISPLAY_REGEX: LazyLock = - LazyLock::new(|| regex::Regex::new(r"( @ )(.+)(\.pyi?:\d)").unwrap()); + LazyLock::new(|| regex::Regex::new(r"( @ )([^\.]+?)(\.pyi?:\d)").unwrap()); fn normalize_path_captures(path_captures: ®ex::Captures) -> String { let normalized_path = std::path::Path::new(&path_captures[2]) @@ -360,6 +360,8 @@ impl Matcher { return false; }; + let primary_annotation = normalize_paths(primary_annotation); + // reveal_type, reveal_protocol_interface if matches!( primary_message,