Skip to content

Commit f51819b

Browse files
committed
[ty] Prefer declared base class attribute over inferred attribute on subclass
1 parent 150ea92 commit f51819b

File tree

5 files changed

+162
-86
lines changed

5 files changed

+162
-86
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -901,24 +901,21 @@ reveal_type(Derived().redeclared_with_wider_type) # revealed: str | int | None
901901
reveal_type(Derived.overwritten_in_subclass_body) # revealed: Unknown | None
902902
reveal_type(Derived().overwritten_in_subclass_body) # revealed: Unknown | None | str
903903

904-
# TODO: Both of these should be `str`
905904
reveal_type(Derived.overwritten_in_subclass_method) # revealed: str
906-
reveal_type(Derived().overwritten_in_subclass_method) # revealed: str | Unknown | None
905+
reveal_type(Derived().overwritten_in_subclass_method) # revealed: str
907906

908907
reveal_type(Derived().pure_attribute) # revealed: str | None
909908

910909
# TODO: This should be `str`
911910
reveal_type(Derived().pure_overwritten_in_subclass_body) # revealed: Unknown | None | str
912911

913-
# TODO: This should be `str`
914-
reveal_type(Derived().pure_overwritten_in_subclass_method) # revealed: Unknown | None
912+
reveal_type(Derived().pure_overwritten_in_subclass_method) # revealed: str
915913

916914
# TODO: Both of these should be `Unknown | Literal["intermediate", "base"]`
917915
reveal_type(Derived.undeclared) # revealed: Unknown | Literal["intermediate"]
918916
reveal_type(Derived().undeclared) # revealed: Unknown | Literal["intermediate"]
919917

920-
# TODO: This should be `Unknown | Literal["intermediate", "base"]`
921-
reveal_type(Derived().pure_undeclared) # revealed: Unknown | Literal["intermediate"]
918+
reveal_type(Derived().pure_undeclared) # revealed: Unknown | Literal["intermediate", "base"]
922919
```
923920

924921
## Accessing attributes on class objects

crates/ty_python_semantic/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod db;
3434
mod dunder_all;
3535
pub mod lint;
3636
pub(crate) mod list;
37+
mod member;
3738
mod module_name;
3839
mod module_resolver;
3940
mod node_key;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use crate::{
2+
place::{Place, PlaceAndQualifiers},
3+
types::Type,
4+
};
5+
6+
/// The return type of certain member-lookup operations. Contains information
7+
/// about the type, type qualifiers, boundness/declaredness, and additional
8+
/// metadata (e.g. whether or not the member was declared)
9+
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
10+
pub(crate) struct Member<'db> {
11+
/// Type, qualifiers, and boundness information of this member
12+
pub(crate) inner: PlaceAndQualifiers<'db>,
13+
14+
/// Whether or not this member was explicitly declared (e.g. `attr: int = 1`
15+
/// on the class body or `self.attr: int = 1` in a class method), or if the
16+
/// type was inferred (e.g. `attr = 1` on the class body or `self.attr = 1`
17+
/// in a class method).
18+
pub(crate) is_declared: bool,
19+
}
20+
21+
impl Default for Member<'_> {
22+
fn default() -> Self {
23+
Member::undeclared(PlaceAndQualifiers::default())
24+
}
25+
}
26+
27+
impl<'db> Member<'db> {
28+
pub(crate) fn undeclared(inner: PlaceAndQualifiers<'db>) -> Self {
29+
Self {
30+
inner,
31+
is_declared: false,
32+
}
33+
}
34+
35+
pub(crate) fn declared(inner: PlaceAndQualifiers<'db>) -> Self {
36+
Self {
37+
inner,
38+
is_declared: true,
39+
}
40+
}
41+
42+
pub(crate) fn declared_and_bound(ty: Type<'db>) -> Self {
43+
Self::declared(Place::bound(ty).into())
44+
}
45+
46+
pub(crate) fn is_unbound(&self) -> bool {
47+
self.inner.place.is_unbound()
48+
}
49+
50+
/// Represents the absence of a member.
51+
pub(crate) fn unbound() -> Self {
52+
Self::undeclared(PlaceAndQualifiers::default())
53+
}
54+
55+
pub(crate) fn ignore_possibly_unbound(&self) -> Option<Type<'db>> {
56+
self.inner.place.ignore_possibly_unbound()
57+
}
58+
59+
#[must_use]
60+
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self {
61+
Self {
62+
inner: self.inner.map_type(f),
63+
is_declared: self.is_declared,
64+
}
65+
}
66+
}

crates/ty_python_semantic/src/place.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_db::files::File;
22

33
use crate::dunder_all::dunder_all_names;
4+
use crate::member::Member;
45
use crate::module_resolver::{KnownModule, file_to_module};
56
use crate::semantic_index::definition::{Definition, DefinitionState};
67
use crate::semantic_index::place::{PlaceExprRef, ScopedPlaceId};
@@ -232,13 +233,9 @@ pub(crate) fn place<'db>(
232233
)
233234
}
234235

235-
/// Infer the public type of a class symbol (its type as seen from outside its scope) in the given
236+
/// Infer the public type of a class member/symbol (its type as seen from outside its scope) in the given
236237
/// `scope`.
237-
pub(crate) fn class_symbol<'db>(
238-
db: &'db dyn Db,
239-
scope: ScopeId<'db>,
240-
name: &str,
241-
) -> PlaceAndQualifiers<'db> {
238+
pub(crate) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Member<'db> {
242239
place_table(db, scope)
243240
.symbol_id(name)
244241
.map(|symbol_id| {
@@ -252,7 +249,7 @@ pub(crate) fn class_symbol<'db>(
252249

253250
if !place_and_quals.place.is_unbound() && !place_and_quals.is_init_var() {
254251
// Trust the declared type if we see a class-level declaration
255-
return place_and_quals;
252+
return Member::declared(place_and_quals);
256253
}
257254

258255
if let PlaceAndQualifiers {
@@ -267,14 +264,14 @@ pub(crate) fn class_symbol<'db>(
267264

268265
// TODO: we should not need to calculate inferred type second time. This is a temporary
269266
// solution until the notion of Boundness and Declaredness is split. See #16036, #16264
270-
match inferred {
267+
Member::undeclared(match inferred {
271268
Place::Unbound => Place::Unbound.with_qualifiers(qualifiers),
272269
Place::Type(_, boundness) => {
273270
Place::Type(ty, boundness).with_qualifiers(qualifiers)
274271
}
275-
}
272+
})
276273
} else {
277-
Place::Unbound.into()
274+
Member::unbound()
278275
}
279276
})
280277
.unwrap_or_default()

0 commit comments

Comments
 (0)