Skip to content

Commit ceab895

Browse files
committed
[ty] No boundness analysis for implicit instance attributes
1 parent e586f6d commit ceab895

File tree

6 files changed

+21
-83
lines changed

6 files changed

+21
-83
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,8 @@ class C:
571571
if (2 + 3) < 4:
572572
self.x: str = "a"
573573

574-
# error: [unresolved-attribute]
575-
reveal_type(C().x) # revealed: Unknown
574+
# TODO: this would ideally raise an `unresolved-attribute` error
575+
reveal_type(C().x) # revealed: str
576576
```
577577

578578
```py
@@ -600,9 +600,10 @@ class C:
600600
def set_e(self, e: str) -> None:
601601
self.e = e
602602

603-
reveal_type(C(True).a) # revealed: Unknown | Literal[1]
604-
# error: [unresolved-attribute]
605-
reveal_type(C(True).b) # revealed: Unknown
603+
# TODO: this would ideally be `Unknown | Literal[1]`
604+
reveal_type(C(True).a) # revealed: Unknown | Literal[1, "a"]
605+
# TODO: this would ideally raise an `unresolved-attribute` error
606+
reveal_type(C(True).b) # revealed: Unknown | Literal[2]
606607
reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str
607608
# Ideally, this would just be `Unknown | Literal[5]`, but we currently do not
608609
# attempt to analyze control flow within methods more closely. All reachable

crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,8 @@ impl ReachabilityConstraints {
820820
}
821821

822822
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
823+
let _span = tracing::trace_span!("analyze_single", ?predicate).entered();
824+
823825
match predicate.node {
824826
PredicateNode::Expression(test_expr) => {
825827
static_expression_truthiness(db, test_expr).negate_if(!predicate.is_positive)

crates/ty_python_semantic/src/semantic_index/use_def.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -598,18 +598,6 @@ impl<'db> UseDefMap<'db> {
598598
.is_always_false()
599599
}
600600

601-
pub(crate) fn declaration_reachability(
602-
&self,
603-
db: &dyn crate::Db,
604-
declaration: &DeclarationWithConstraint<'db>,
605-
) -> Truthiness {
606-
self.reachability_constraints.evaluate(
607-
db,
608-
&self.predicates,
609-
declaration.reachability_constraint,
610-
)
611-
}
612-
613601
pub(crate) fn binding_reachability(
614602
&self,
615603
db: &dyn crate::Db,

crates/ty_python_semantic/src/types.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6910,10 +6910,10 @@ bitflags! {
69106910
const NOT_REQUIRED = 1 << 4;
69116911
/// `typing_extensions.ReadOnly`
69126912
const READ_ONLY = 1 << 5;
6913-
/// An implicit instance attribute which is possibly unbound according
6914-
/// to local control flow within the method it is defined in. This flag
6915-
/// overrules the `Boundness` information on `PlaceAndQualifiers`.
6916-
const POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE = 1 << 6;
6913+
/// A non-standard type qualifier that marks implicit instance attributes, i.e.
6914+
/// instance attributes that are only implicitly defined via `self.x = …` in
6915+
/// the body of a class method.
6916+
const IMPLICIT_INSTANCE_ATTRIBUTE = 1 << 6;
69176917
}
69186918
}
69196919

@@ -8663,14 +8663,6 @@ impl Truthiness {
86638663
if condition { self.negate() } else { self }
86648664
}
86658665

8666-
pub(crate) fn and(self, other: Self) -> Self {
8667-
match (self, other) {
8668-
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue,
8669-
(Truthiness::AlwaysFalse, _) | (_, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse,
8670-
_ => Truthiness::Ambiguous,
8671-
}
8672-
}
8673-
86748666
pub(crate) fn or(self, other: Self) -> Self {
86758667
match (self, other) {
86768668
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse,

crates/ty_python_semantic/src/types/class.rs

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,7 +2686,7 @@ impl<'db> ClassLiteral<'db> {
26862686
// that attribute. We include `Unknown` in that union to account for the fact that the
26872687
// attribute might be externally modified.
26882688
let mut union_of_inferred_types = UnionBuilder::new(db);
2689-
let mut qualifiers = TypeQualifiers::empty();
2689+
let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE;
26902690

26912691
let mut is_attribute_bound = false;
26922692

@@ -2736,20 +2736,11 @@ impl<'db> ClassLiteral<'db> {
27362736
// self.name: <annotation>
27372737
// self.name: <annotation> = …
27382738

2739-
let reachability = use_def_map(db, method_scope)
2740-
.declaration_reachability(db, &attribute_declaration);
2741-
2742-
if reachability.is_always_false() {
2743-
continue;
2744-
}
2745-
27462739
let annotation = declaration_type(db, declaration);
2747-
let mut annotation =
2748-
Place::bound(annotation.inner).with_qualifiers(annotation.qualifiers);
2740+
let annotation = Place::bound(annotation.inner).with_qualifiers(
2741+
annotation.qualifiers | TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE,
2742+
);
27492743

2750-
if reachability.is_ambiguous() {
2751-
annotation.qualifiers |= TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE;
2752-
}
27532744
if let Some(all_qualifiers) = annotation.is_bare_final() {
27542745
if let Some(value) = assignment.value(&module) {
27552746
// If we see an annotated assignment with a bare `Final` as in
@@ -2781,8 +2772,6 @@ impl<'db> ClassLiteral<'db> {
27812772
continue;
27822773
}
27832774

2784-
let method_map = use_def_map(db, method_scope);
2785-
27862775
// The attribute assignment inherits the reachability of the method which contains it
27872776
let is_method_reachable =
27882777
if let Some(method_def) = method_scope.node(db).as_function(&module) {
@@ -2802,53 +2791,16 @@ impl<'db> ClassLiteral<'db> {
28022791
continue;
28032792
}
28042793

2805-
// Storage for the implicit `DefinitionState::Undefined` binding. If present, it
2806-
// will be the first binding in the `attribute_assignments` iterator.
2807-
let mut unbound_binding = None;
2808-
28092794
for attribute_assignment in attribute_assignments {
28102795
if let DefinitionState::Undefined = attribute_assignment.binding {
2811-
// Store the implicit unbound binding here so that we can delay the
2812-
// computation of `unbound_reachability` to the point when we actually
2813-
// need it. This is an optimization for the common case where the
2814-
// `unbound` binding is the only binding of the `name` attribute,
2815-
// i.e. if there is no `self.name = …` assignment in this method.
2816-
unbound_binding = Some(attribute_assignment);
28172796
continue;
28182797
}
28192798

28202799
let DefinitionState::Defined(binding) = attribute_assignment.binding else {
28212800
continue;
28222801
};
2823-
match method_map
2824-
.binding_reachability(db, &attribute_assignment)
2825-
.and(is_method_reachable)
2826-
{
2827-
Truthiness::AlwaysTrue => {
2828-
is_attribute_bound = true;
2829-
}
2830-
Truthiness::Ambiguous => {
2831-
is_attribute_bound = true;
2832-
qualifiers |= TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE;
2833-
}
2834-
Truthiness::AlwaysFalse => {
2835-
continue;
2836-
}
2837-
}
2838-
2839-
// There is at least one attribute assignment that may be reachable, so if `unbound_reachability` is
2840-
// always false then this attribute is considered bound.
2841-
// TODO: this is incomplete logic since the attributes bound after termination are considered reachable.
2842-
let unbound_reachability = unbound_binding
2843-
.as_ref()
2844-
.map(|binding| method_map.binding_reachability(db, binding))
2845-
.unwrap_or(Truthiness::AlwaysFalse);
28462802

2847-
if unbound_reachability
2848-
.negate()
2849-
.and(is_method_reachable)
2850-
.is_always_true()
2851-
{
2803+
if !is_method_reachable.is_always_false() {
28522804
is_attribute_bound = true;
28532805
}
28542806

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7199,10 +7199,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
71997199
}
72007200
}
72017201
let fallback_place = value_type.member(db, &attr.id);
7202+
// Exclude non-definitely-bound places for purposes of reachability
7203+
// analysis. We currently do not perform boundness analysis for implicit
7204+
// instance attributes, so we exclude them here as well.
72027205
if !fallback_place.place.is_definitely_bound()
72037206
|| fallback_place
72047207
.qualifiers
7205-
.contains(TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE)
7208+
.contains(TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE)
72067209
{
72077210
self.all_definitely_bound = false;
72087211
}

0 commit comments

Comments
 (0)