mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-22 16:22:52 +00:00
[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:
parent
b6b96d75eb
commit
1f8297cfe6
19 changed files with 102 additions and 63 deletions
|
@ -30,7 +30,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
|
|||
success: false
|
||||
exit_code: 1
|
||||
----- 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
|
||||
|
|
||||
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
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-attribute]: Type `<module 'os'>` has no attribute `grantpt`
|
||||
error[unresolved-attribute]: Module `os` has no member `grantpt`
|
||||
--> main.py:4:1
|
||||
|
|
||||
2 | import os
|
||||
|
|
|
@ -1112,11 +1112,11 @@ print(sys.last_exc, os.getegid())
|
|||
assert_eq!(diagnostics.len(), 2);
|
||||
assert_eq!(
|
||||
diagnostics[0].primary_message(),
|
||||
"Type `<module 'sys'>` has no attribute `last_exc`"
|
||||
"Module `sys` has no member `last_exc`"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics[1].primary_message(),
|
||||
"Type `<module 'os'>` has no attribute `getegid`"
|
||||
"Module `os` has no member `getegid`"
|
||||
);
|
||||
|
||||
// Change the python version
|
||||
|
|
|
@ -366,7 +366,7 @@ on function-like callables:
|
|||
|
||||
```py
|
||||
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__
|
||||
|
||||
# error: [unresolved-attribute] "Unresolved attribute `__qualname__` on type `() -> None`."
|
||||
|
|
|
@ -1260,13 +1260,13 @@ def _(flag1: bool, flag2: bool):
|
|||
|
||||
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]
|
||||
|
||||
# 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
|
||||
|
||||
# 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]
|
||||
|
||||
# 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
|
||||
|
||||
# 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]
|
||||
|
||||
# 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,
|
||||
# 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]
|
||||
|
||||
# error: [possibly-missing-attribute]
|
||||
|
@ -1433,7 +1433,7 @@ def _(flag: bool):
|
|||
class 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
|
||||
|
||||
# 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.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
|
||||
```
|
||||
|
||||
|
|
|
@ -311,7 +311,7 @@ reveal_type(C.f(1)) # revealed: str
|
|||
The method `f` can not be accessed from an instance of the class:
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute] "Type `C` has no attribute `f`"
|
||||
# error: [unresolved-attribute] "Object of type `C` has no attribute `f`"
|
||||
C().f
|
||||
```
|
||||
|
||||
|
|
|
@ -308,7 +308,7 @@ class B(A): ...
|
|||
|
||||
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
|
||||
```
|
||||
|
||||
|
@ -436,7 +436,7 @@ def f(x: C | D):
|
|||
s = super(A, x)
|
||||
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
|
||||
|
||||
def f(flag: bool):
|
||||
|
@ -476,7 +476,7 @@ def f(flag: bool):
|
|||
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
|
||||
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
|
||||
```
|
||||
|
||||
|
@ -619,7 +619,7 @@ class B(A):
|
|||
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||
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
|
||||
```
|
||||
|
||||
|
|
|
@ -758,16 +758,16 @@ def _(flag: bool):
|
|||
non_data: NonDataDescriptor = NonDataDescriptor()
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
|
|
|
@ -26,9 +26,9 @@ def _(flag: bool):
|
|||
|
||||
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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
|
|
@ -247,7 +247,7 @@ X: int = 42
|
|||
from . import foo
|
||||
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
|
||||
```
|
||||
|
||||
|
|
|
@ -95,6 +95,6 @@ def f(x: object):
|
|||
reveal_type(x.__str__) # revealed: bound method object.__str__() -> str
|
||||
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
|
||||
```
|
||||
|
|
|
@ -16,7 +16,7 @@ class C:
|
|||
if flag:
|
||||
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.y) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
|
|
@ -26,7 +26,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
|||
# 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
|
||||
|
|
||||
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
|
||||
|
|
||||
8 | instance = C()
|
||||
|
|
|
@ -23,7 +23,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
|
|||
# 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
|
||||
|
|
||||
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
|
||||
|
|
||||
4 | reveal_type(datetime.UTC) # revealed: Unknown
|
||||
|
|
|
@ -118,7 +118,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
|
|||
# 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
|
||||
|
|
||||
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
|
||||
|
|
||||
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
|
||||
|
|
||||
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
|
||||
|
|
||||
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
|
||||
|
|
||||
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
|
||||
|
|
||||
25 | super(A, C()).a # error: [unresolved-attribute]
|
||||
|
|
|
@ -618,7 +618,7 @@ import importlib
|
|||
from module2 import importlib as other_importlib
|
||||
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(other_importlib.abc) # revealed: <module 'importlib.abc'>
|
||||
|
|
|
@ -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:
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`"
|
||||
# error: [unresolved-attribute] "Class `Person` has no attribute `name`"
|
||||
Person.name
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -2269,10 +2269,25 @@ pub(super) fn report_possibly_missing_attribute(
|
|||
let Some(builder) = context.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, target) else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Attribute `{attribute}` on type `{}` may be missing",
|
||||
object_ty.display(context.db()),
|
||||
));
|
||||
let db = context.db();
|
||||
match object_ty {
|
||||
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) {
|
||||
|
|
|
@ -814,6 +814,10 @@ impl Display for DisplayFunctionType<'_> {
|
|||
}
|
||||
|
||||
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(
|
||||
&'db self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -7628,13 +7628,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
let diagnostic = builder.into_diagnostic(
|
||||
format_args!(
|
||||
"Type `{}` has no attribute `{}`",
|
||||
let diagnostic = match value_type {
|
||||
Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
|
||||
"Module `{}` has no member `{}`",
|
||||
module.module(db).name(db),
|
||||
&attr.id
|
||||
)),
|
||||
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
|
||||
"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
|
||||
),
|
||||
);
|
||||
&attr.id
|
||||
)),
|
||||
};
|
||||
hint_if_stdlib_attribute_exists_on_other_versions(db, diagnostic, &value_type, attr);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue