[ty] Improve error messages for unresolved attribute diagnostics (#20963)

## Summary

- Type checkers (and type-checker authors) think in terms of types, but
I think most Python users think in terms of values. Rather than saying
that a _type_ `X` "has no attribute `foo`" (which I think sounds strange
to many users), say that "an object of type `X` has no attribute `foo`"
- Special-case certain types so that the diagnostic messages read more
like normal English: rather than saying "Type `<class 'Foo'>` has no
attribute `bar`" or "Object of type `<class 'Foo'>` has no attribute
`bar`", just say "Class `Foo` has no attribute `bar`"

## Test Plan

Mdtests and snapshots updated
This commit is contained in:
Alex Waygood 2025-10-19 10:58:25 +01:00 committed by GitHub
parent b6b96d75eb
commit 1f8297cfe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 102 additions and 63 deletions

View file

@ -30,7 +30,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
error[unresolved-attribute]: Type `<module 'sys'>` has no attribute `last_exc` error[unresolved-attribute]: Module `sys` has no member `last_exc`
--> test.py:5:7 --> test.py:5:7
| |
4 | # Access `sys.last_exc` that was only added in Python 3.12 4 | # Access `sys.last_exc` that was only added in Python 3.12
@ -962,7 +962,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
error[unresolved-attribute]: Type `<module 'os'>` has no attribute `grantpt` error[unresolved-attribute]: Module `os` has no member `grantpt`
--> main.py:4:1 --> main.py:4:1
| |
2 | import os 2 | import os

View file

@ -1112,11 +1112,11 @@ print(sys.last_exc, os.getegid())
assert_eq!(diagnostics.len(), 2); assert_eq!(diagnostics.len(), 2);
assert_eq!( assert_eq!(
diagnostics[0].primary_message(), diagnostics[0].primary_message(),
"Type `<module 'sys'>` has no attribute `last_exc`" "Module `sys` has no member `last_exc`"
); );
assert_eq!( assert_eq!(
diagnostics[1].primary_message(), diagnostics[1].primary_message(),
"Type `<module 'os'>` has no attribute `getegid`" "Module `os` has no member `getegid`"
); );
// Change the python version // Change the python version

View file

@ -366,7 +366,7 @@ on function-like callables:
```py ```py
def f_wrong(c: Callable[[], None]): def f_wrong(c: Callable[[], None]):
# error: [unresolved-attribute] "Type `() -> None` has no attribute `__qualname__`" # error: [unresolved-attribute] "Object of type `() -> None` has no attribute `__qualname__`"
c.__qualname__ c.__qualname__
# error: [unresolved-attribute] "Unresolved attribute `__qualname__` on type `() -> None`." # error: [unresolved-attribute] "Unresolved attribute `__qualname__` on type `() -> None`."

View file

@ -1260,13 +1260,13 @@ def _(flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3 C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` may be missing" # error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
reveal_type(C.x) # revealed: Unknown | Literal[1, 3] reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>`" # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
C.x = 100 C.x = 100
# error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing" # error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `C1 | C2 | C3`"
reveal_type(C().x) # revealed: Unknown | Literal[1, 3] reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`" # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`"
@ -1292,7 +1292,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3 C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` may be missing" # error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3] reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-missing-attribute] # error: [possibly-missing-attribute]
@ -1300,7 +1300,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
# Note: we might want to consider ignoring possibly-missing diagnostics for instance attributes eventually, # Note: we might want to consider ignoring possibly-missing diagnostics for instance attributes eventually,
# see the "Possibly unbound/undeclared instance attribute" section below. # see the "Possibly unbound/undeclared instance attribute" section below.
# error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing" # error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `C1 | C2 | C3`"
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3] reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-missing-attribute] # error: [possibly-missing-attribute]
@ -1433,7 +1433,7 @@ def _(flag: bool):
class C2: ... class C2: ...
C = C1 if flag else C2 C = C1 if flag else C2
# error: [unresolved-attribute] "Type `<class 'C1'> | <class 'C2'>` has no attribute `x`" # error: [unresolved-attribute] "Object of type `<class 'C1'> | <class 'C2'>` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown reveal_type(C.x) # revealed: Unknown
# TODO: This should ideally be a `unresolved-attribute` error. We need better union # TODO: This should ideally be a `unresolved-attribute` error. We need better union
@ -1771,7 +1771,7 @@ reveal_type(date.day) # revealed: int
reveal_type(date.month) # revealed: int reveal_type(date.month) # revealed: int
reveal_type(date.year) # revealed: int reveal_type(date.year) # revealed: int
# error: [unresolved-attribute] "Type `Date` has no attribute `century`" # error: [unresolved-attribute] "Object of type `Date` has no attribute `century`"
reveal_type(date.century) # revealed: Unknown reveal_type(date.century) # revealed: Unknown
``` ```

View file

@ -311,7 +311,7 @@ reveal_type(C.f(1)) # revealed: str
The method `f` can not be accessed from an instance of the class: The method `f` can not be accessed from an instance of the class:
```py ```py
# error: [unresolved-attribute] "Type `C` has no attribute `f`" # error: [unresolved-attribute] "Object of type `C` has no attribute `f`"
C().f C().f
``` ```

View file

@ -308,7 +308,7 @@ class B(A): ...
reveal_type(super(B)) # revealed: super reveal_type(super(B)) # revealed: super
# error: [unresolved-attribute] "Type `super` has no attribute `a`" # error: [unresolved-attribute] "Object of type `super` has no attribute `a`"
super(B).a super(B).a
``` ```
@ -436,7 +436,7 @@ def f(x: C | D):
s = super(A, x) s = super(A, x)
reveal_type(s) # revealed: <super: <class 'A'>, C> | <super: <class 'A'>, D> reveal_type(s) # revealed: <super: <class 'A'>, C> | <super: <class 'A'>, D>
# error: [possibly-missing-attribute] "Attribute `b` on type `<super: <class 'A'>, C> | <super: <class 'A'>, D>` may be missing" # error: [possibly-missing-attribute] "Attribute `b` may be missing on object of type `<super: <class 'A'>, C> | <super: <class 'A'>, D>`"
s.b s.b
def f(flag: bool): def f(flag: bool):
@ -476,7 +476,7 @@ def f(flag: bool):
reveal_type(s.x) # revealed: Unknown | Literal[1, 2] reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
reveal_type(s.y) # revealed: int | str reveal_type(s.y) # revealed: int | str
# error: [possibly-missing-attribute] "Attribute `a` on type `<super: <class 'B'>, B> | <super: <class 'D'>, D>` may be missing" # error: [possibly-missing-attribute] "Attribute `a` may be missing on object of type `<super: <class 'B'>, B> | <super: <class 'D'>, D>`"
reveal_type(s.a) # revealed: str reveal_type(s.a) # revealed: str
``` ```
@ -619,7 +619,7 @@ class B(A):
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error # TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
super().a super().a
# error: [unresolved-attribute] "Type `<super: <class 'B'>, B>` has no attribute `a`" # error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
super(B, B(42)).a super(B, B(42)).a
``` ```

View file

@ -758,16 +758,16 @@ def _(flag: bool):
non_data: NonDataDescriptor = NonDataDescriptor() non_data: NonDataDescriptor = NonDataDescriptor()
data: DataDescriptor = DataDescriptor() data: DataDescriptor = DataDescriptor()
# error: [possibly-missing-attribute] "Attribute `non_data` on type `<class 'PossiblyUnbound'>` may be missing" # error: [possibly-missing-attribute] "Attribute `non_data` may be missing on class `PossiblyUnbound`"
reveal_type(PossiblyUnbound.non_data) # revealed: int reveal_type(PossiblyUnbound.non_data) # revealed: int
# error: [possibly-missing-attribute] "Attribute `non_data` on type `PossiblyUnbound` may be missing" # error: [possibly-missing-attribute] "Attribute `non_data` may be missing on object of type `PossiblyUnbound`"
reveal_type(PossiblyUnbound().non_data) # revealed: int reveal_type(PossiblyUnbound().non_data) # revealed: int
# error: [possibly-missing-attribute] "Attribute `data` on type `<class 'PossiblyUnbound'>` may be missing" # error: [possibly-missing-attribute] "Attribute `data` may be missing on class `PossiblyUnbound`"
reveal_type(PossiblyUnbound.data) # revealed: int reveal_type(PossiblyUnbound.data) # revealed: int
# error: [possibly-missing-attribute] "Attribute `data` on type `PossiblyUnbound` may be missing" # error: [possibly-missing-attribute] "Attribute `data` may be missing on object of type `PossiblyUnbound`"
reveal_type(PossiblyUnbound().data) # revealed: int reveal_type(PossiblyUnbound().data) # revealed: int
``` ```

View file

@ -26,9 +26,9 @@ def _(flag: bool):
reveal_type(A.union_declared) # revealed: int | str reveal_type(A.union_declared) # revealed: int | str
# error: [possibly-missing-attribute] "Attribute `possibly_unbound` on type `<class 'A'>` may be missing" # error: [possibly-missing-attribute] "Attribute `possibly_unbound` may be missing on class `A`"
reveal_type(A.possibly_unbound) # revealed: str reveal_type(A.possibly_unbound) # revealed: str
# error: [unresolved-attribute] "Type `<class 'A'>` has no attribute `non_existent`" # error: [unresolved-attribute] "Class `A` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown reveal_type(A.non_existent) # revealed: Unknown
``` ```

View file

@ -247,7 +247,7 @@ X: int = 42
from . import foo from . import foo
import package import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`" # error: [unresolved-attribute] "Module `package` has no member `foo`"
reveal_type(package.foo.X) # revealed: Unknown reveal_type(package.foo.X) # revealed: Unknown
``` ```

View file

@ -95,6 +95,6 @@ def f(x: object):
reveal_type(x.__str__) # revealed: bound method object.__str__() -> str reveal_type(x.__str__) # revealed: bound method object.__str__() -> str
reveal_type(x.__dict__) # revealed: dict[str, Any] reveal_type(x.__dict__) # revealed: dict[str, Any]
# error: [unresolved-attribute] "Type `<Protocol with members '__qualname__'>` has no attribute `foo`" # error: [unresolved-attribute] "Object of type `<Protocol with members '__qualname__'>` has no attribute `foo`"
reveal_type(x.foo) # revealed: Unknown reveal_type(x.foo) # revealed: Unknown
``` ```

View file

@ -16,7 +16,7 @@ class C:
if flag: if flag:
x = 2 x = 2
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C'>` may be missing" # error: [possibly-missing-attribute] "Attribute `x` may be missing on class `C`"
reveal_type(C.x) # revealed: Unknown | Literal[2] reveal_type(C.x) # revealed: Unknown | Literal[2]
reveal_type(C.y) # revealed: Unknown | Literal[1] reveal_type(C.y) # revealed: Unknown | Literal[1]
``` ```

View file

@ -26,7 +26,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
# Diagnostics # Diagnostics
``` ```
warning[possibly-missing-attribute]: Attribute `attr` on type `<class 'C'>` may be missing warning[possibly-missing-attribute]: Attribute `attr` may be missing on class `C`
--> src/mdtest_snippet.py:6:5 --> src/mdtest_snippet.py:6:5
| |
4 | attr: int = 0 4 | attr: int = 0
@ -41,7 +41,7 @@ info: rule `possibly-missing-attribute` is enabled by default
``` ```
``` ```
warning[possibly-missing-attribute]: Attribute `attr` on type `C` may be missing warning[possibly-missing-attribute]: Attribute `attr` may be missing on object of type `C`
--> src/mdtest_snippet.py:9:5 --> src/mdtest_snippet.py:9:5
| |
8 | instance = C() 8 | instance = C()

View file

@ -23,7 +23,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
# Diagnostics # Diagnostics
``` ```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `UTC` error[unresolved-attribute]: Module `datetime` has no member `UTC`
--> src/main.py:4:13 --> src/main.py:4:13
| |
3 | # error: [unresolved-attribute] 3 | # error: [unresolved-attribute]
@ -38,7 +38,7 @@ info: rule `unresolved-attribute` is enabled by default
``` ```
``` ```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `fakenotreal` error[unresolved-attribute]: Module `datetime` has no member `fakenotreal`
--> src/main.py:6:13 --> src/main.py:6:13
| |
4 | reveal_type(datetime.UTC) # revealed: Unknown 4 | reveal_type(datetime.UTC) # revealed: Unknown

View file

@ -118,7 +118,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
# Diagnostics # Diagnostics
``` ```
error[unresolved-attribute]: Type `<super: <class 'C'>, C>` has no attribute `c` error[unresolved-attribute]: Object of type `<super: <class 'C'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:19:1 --> src/mdtest_snippet.py:19:1
| |
17 | super(C, C()).a 17 | super(C, C()).a
@ -133,7 +133,7 @@ info: rule `unresolved-attribute` is enabled by default
``` ```
``` ```
error[unresolved-attribute]: Type `<super: <class 'B'>, C>` has no attribute `b` error[unresolved-attribute]: Object of type `<super: <class 'B'>, C>` has no attribute `b`
--> src/mdtest_snippet.py:22:1 --> src/mdtest_snippet.py:22:1
| |
21 | super(B, C()).a 21 | super(B, C()).a
@ -146,7 +146,7 @@ info: rule `unresolved-attribute` is enabled by default
``` ```
``` ```
error[unresolved-attribute]: Type `<super: <class 'B'>, C>` has no attribute `c` error[unresolved-attribute]: Object of type `<super: <class 'B'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:23:1 --> src/mdtest_snippet.py:23:1
| |
21 | super(B, C()).a 21 | super(B, C()).a
@ -161,7 +161,7 @@ info: rule `unresolved-attribute` is enabled by default
``` ```
``` ```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `a` error[unresolved-attribute]: Object of type `<super: <class 'A'>, C>` has no attribute `a`
--> src/mdtest_snippet.py:25:1 --> src/mdtest_snippet.py:25:1
| |
23 | super(B, C()).c # error: [unresolved-attribute] 23 | super(B, C()).c # error: [unresolved-attribute]
@ -176,7 +176,7 @@ info: rule `unresolved-attribute` is enabled by default
``` ```
``` ```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `b` error[unresolved-attribute]: Object of type `<super: <class 'A'>, C>` has no attribute `b`
--> src/mdtest_snippet.py:26:1 --> src/mdtest_snippet.py:26:1
| |
25 | super(A, C()).a # error: [unresolved-attribute] 25 | super(A, C()).a # error: [unresolved-attribute]
@ -189,7 +189,7 @@ info: rule `unresolved-attribute` is enabled by default
``` ```
``` ```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `c` error[unresolved-attribute]: Object of type `<super: <class 'A'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:27:1 --> src/mdtest_snippet.py:27:1
| |
25 | super(A, C()).a # error: [unresolved-attribute] 25 | super(A, C()).a # error: [unresolved-attribute]

View file

@ -618,7 +618,7 @@ import importlib
from module2 import importlib as other_importlib from module2 import importlib as other_importlib
from ty_extensions import TypeOf, static_assert, is_equivalent_to from ty_extensions import TypeOf, static_assert, is_equivalent_to
# error: [unresolved-attribute] "Type `<module 'importlib'>` has no attribute `abc`" # error: [unresolved-attribute] "Module `importlib` has no member `abc`"
reveal_type(importlib.abc) # revealed: Unknown reveal_type(importlib.abc) # revealed: Unknown
reveal_type(other_importlib.abc) # revealed: <module 'importlib.abc'> reveal_type(other_importlib.abc) # revealed: <module 'importlib.abc'>

View file

@ -672,18 +672,18 @@ Also, the "attributes" on the class definition can not be accessed. Neither on t
on inhabitants of the type defined by the class: on inhabitants of the type defined by the class:
```py ```py
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`" # error: [unresolved-attribute] "Class `Person` has no attribute `name`"
Person.name Person.name
def _(P: type[Person]): def _(P: type[Person]):
# error: [unresolved-attribute] "Type `type[Person]` has no attribute `name`" # error: [unresolved-attribute] "Object of type `type[Person]` has no attribute `name`"
P.name P.name
def _(p: Person) -> None: def _(p: Person) -> None:
# error: [unresolved-attribute] "Type `Person` has no attribute `name`" # error: [unresolved-attribute] "Object of type `Person` has no attribute `name`"
p.name p.name
type(p).name # error: [unresolved-attribute] "Type `<class 'dict[str, object]'>` has no attribute `name`" type(p).name # error: [unresolved-attribute] "Class `dict[str, object]` has no attribute `name`"
``` ```
## Special properties ## Special properties

View file

@ -2269,10 +2269,25 @@ pub(super) fn report_possibly_missing_attribute(
let Some(builder) = context.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, target) else { let Some(builder) = context.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, target) else {
return; return;
}; };
builder.into_diagnostic(format_args!( let db = context.db();
"Attribute `{attribute}` on type `{}` may be missing", match object_ty {
object_ty.display(context.db()), Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
)); "Member `{attribute}` may be missing on module `{}`",
module.module(db).name(db),
)),
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
"Attribute `{attribute}` may be missing on class `{}`",
class.name(db),
)),
Type::GenericAlias(alias) => builder.into_diagnostic(format_args!(
"Attribute `{attribute}` may be missing on class `{}`",
alias.display(db),
)),
_ => builder.into_diagnostic(format_args!(
"Attribute `{attribute}` may be missing on object of type `{}`",
object_ty.display(db),
)),
};
} }
pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) {

View file

@ -814,6 +814,10 @@ impl Display for DisplayFunctionType<'_> {
} }
impl<'db> GenericAlias<'db> { impl<'db> GenericAlias<'db> {
pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> {
self.display_with(db, DisplaySettings::default())
}
pub(crate) fn display_with( pub(crate) fn display_with(
&'db self, &'db self,
db: &'db dyn Db, db: &'db dyn Db,

View file

@ -7618,25 +7618,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.context .context
.report_lint(&UNRESOLVED_ATTRIBUTE, attribute) .report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
{ {
if bound_on_instance { if bound_on_instance {
builder.into_diagnostic( builder.into_diagnostic(
format_args!( format_args!(
"Attribute `{}` can only be accessed on instances, \ "Attribute `{}` can only be accessed on instances, \
not on the class object `{}` itself.", not on the class object `{}` itself.",
attr.id, attr.id,
value_type.display(db) value_type.display(db)
), ),
); );
} else { } else {
let diagnostic = builder.into_diagnostic( let diagnostic = match value_type {
format_args!( Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
"Type `{}` has no attribute `{}`", "Module `{}` has no member `{}`",
value_type.display(db), module.module(db).name(db),
attr.id &attr.id
), )),
); Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
hint_if_stdlib_attribute_exists_on_other_versions(db, diagnostic, &value_type, attr); "Class `{}` has no attribute `{}`",
} class.name(db),
&attr.id
)),
Type::GenericAlias(alias) => builder.into_diagnostic(format_args!(
"Class `{}` has no attribute `{}`",
alias.display(db),
&attr.id
)),
Type::FunctionLiteral(function) => builder.into_diagnostic(format_args!(
"Function `{}` has no attribute `{}`",
function.name(db),
&attr.id
)),
_ => builder.into_diagnostic(format_args!(
"Object of type `{}` has no attribute `{}`",
value_type.display(db),
&attr.id
)),
};
hint_if_stdlib_attribute_exists_on_other_versions(db, diagnostic, &value_type, attr);
}
} }
} }