[ty] Improve disambiguation of types (#22547)

This commit is contained in:
Alex Waygood
2026-01-13 14:56:56 +00:00
committed by GitHub
parent 3878701265
commit c7b41060f4
13 changed files with 50 additions and 21 deletions

View File

@@ -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 `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8:15'>`"
C1.y = "problematic"
class C2:

View File

@@ -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.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:8`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9`"
# error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:8:12`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9:12`"
inner(cls2())
```

View File

@@ -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()
```

View File

@@ -398,10 +398,10 @@ if returns_bool():
else:
class B(Y, X): ...
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
# revealed: (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
reveal_mro(B)
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>`"
class Z(A, B): ...
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)

View File

@@ -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()
```

View File

@@ -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 `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.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.

View File

@@ -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

View File

@@ -67,7 +67,7 @@ warning[unsupported-base]: Unsupported class base
25 | class C: ...
26 |
27 | class D(C): ... # error: [unsupported-base]
| ^ Has type `<class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:23'> | <class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:25'>`
| ^ Has type `<class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:23:15'> | <class 'mdtest_snippet.<locals of function 'f'>.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

View File

@@ -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 `<class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:6'> | <class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:8'>`
| ^^^^^^^^^^^^^ Argument has type `<class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:6:15'> | <class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:8:15'>`
|
info: rule `invalid-super-argument` is enabled by default

View File

@@ -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,
}
}
}

View File

@@ -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(())
}

View File

@@ -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()

View File

@@ -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<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"( @ )(.+)(\.pyi?:\d)").unwrap());
LazyLock::new(|| regex::Regex::new(r"( @ )([^\.]+?)(\.pyi?:\d)").unwrap());
fn normalize_path_captures(path_captures: &regex::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,