Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2288,6 +2288,26 @@ class H:
self.x = other.x or self.x
```

An attribute definition can be guarded by a condition involving that attribute. This is a regression
test for <https://github.com/astral-sh/ty/issues/692>:

```py
from typing import Literal

def check(x) -> Literal[False]:
return False

class Toggle:
def __init__(self: "Toggle"):
if not self.x:
self.x: Literal[True] = True
if check(self.y):
self.y = True

reveal_type(Toggle().x) # revealed: Literal[True]
reveal_type(Toggle().y) # revealed: Unknown | Literal[True]
```

### Builtin types attributes

This test can probably be removed eventually, but we currently include it because we do not yet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,25 @@ if True:
from module import symbol
```

## Non-definitely bound symbols in conditions

When a non-definitely bound symbol is used as a (part of a) condition, we always infer an ambiguous
truthiness. If we wouldn't do that, `x` would be considered definitely bound in the following
example:

```py
def _(flag: bool):
if flag:
ALWAYS_TRUE_IF_BOUND = True

# error: [possibly-unresolved-reference] "Name `ALWAYS_TRUE_IF_BOUND` used when possibly not defined"
if True and ALWAYS_TRUE_IF_BOUND:
x = 1

# error: [possibly-unresolved-reference] "Name `x` used when possibly not defined"
x
```

## Unreachable code

A closely related feature is the ability to detect unreachable code. For example, we do not emit a
Expand Down
4 changes: 4 additions & 0 deletions crates/ty_python_semantic/src/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ impl<'db> Place<'db> {
Place::Unbound => Place::Unbound,
}
}

pub(crate) fn is_definitely_bound(&self) -> bool {
matches!(self, Place::Type(_, Boundness::Bound))
}
}

impl<'db> From<LookupResult<'db>> for PlaceAndQualifiers<'db> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ use crate::semantic_index::predicate::{
};
use crate::types::{
IntersectionBuilder, Truthiness, Type, UnionBuilder, UnionType, infer_expression_type,
static_expression_truthiness,
};

/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
Expand Down Expand Up @@ -821,8 +822,7 @@ impl ReachabilityConstraints {
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
match predicate.node {
PredicateNode::Expression(test_expr) => {
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!predicate.is_positive)
static_expression_truthiness(db, test_expr).negate_if(!predicate.is_positive)
}
PredicateNode::ReturnsNever(CallableAndCallExpr {
callable,
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/src/semantic_index/use_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ impl<'db> UseDefMap<'db> {
.is_always_false()
}

pub(crate) fn is_declaration_reachable(
pub(crate) fn declaration_reachability(
&self,
db: &dyn crate::Db,
declaration: &DeclarationWithConstraint<'db>,
Expand All @@ -610,7 +610,7 @@ impl<'db> UseDefMap<'db> {
)
}

pub(crate) fn is_binding_reachable(
pub(crate) fn binding_reachability(
&self,
db: &dyn crate::Db,
binding: &BindingWithConstraints<'_, 'db>,
Expand Down
8 changes: 7 additions & 1 deletion crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub use self::diagnostic::TypeCheckDiagnostics;
pub(crate) use self::diagnostic::register_lints;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
infer_scope_types,
infer_scope_types, static_expression_truthiness,
};
pub(crate) use self::signatures::{CallableSignature, Signature};
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
Expand Down Expand Up @@ -6910,6 +6910,10 @@ bitflags! {
const NOT_REQUIRED = 1 << 4;
/// `typing_extensions.ReadOnly`
const READ_ONLY = 1 << 5;
/// An implicit instance attribute which is possibly unbound according
/// to local control flow within the method it is defined in. This flag
/// overrules the `Boundness` information on `PlaceAndQualifiers`.
const POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE = 1 << 6;
}
}

Expand Down Expand Up @@ -8630,6 +8634,8 @@ pub enum Truthiness {
Ambiguous,
}

impl get_size2::GetSize for Truthiness {}

impl Truthiness {
pub(crate) const fn is_ambiguous(self) -> bool {
matches!(self, Truthiness::Ambiguous)
Expand Down
25 changes: 16 additions & 9 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2736,17 +2736,20 @@ impl<'db> ClassLiteral<'db> {
// self.name: <annotation>
// self.name: <annotation> = …

if use_def_map(db, method_scope)
.is_declaration_reachable(db, &attribute_declaration)
.is_always_false()
{
let reachability = use_def_map(db, method_scope)
.declaration_reachability(db, &attribute_declaration);

if reachability.is_always_false() {
continue;
}

let annotation = declaration_type(db, declaration);
let annotation =
let mut annotation =
Place::bound(annotation.inner).with_qualifiers(annotation.qualifiers);

if reachability.is_ambiguous() {
annotation.qualifiers |= TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE;
}
if let Some(all_qualifiers) = annotation.is_bare_final() {
if let Some(value) = assignment.value(&module) {
// If we see an annotated assignment with a bare `Final` as in
Expand Down Expand Up @@ -2789,7 +2792,7 @@ impl<'db> ClassLiteral<'db> {
.all_reachable_symbol_bindings(method_place)
.find_map(|bind| {
(bind.binding.is_defined_and(|def| def == method))
.then(|| class_map.is_binding_reachable(db, &bind))
.then(|| class_map.binding_reachability(db, &bind))
})
.unwrap_or(Truthiness::AlwaysFalse)
} else {
Expand Down Expand Up @@ -2818,11 +2821,15 @@ impl<'db> ClassLiteral<'db> {
continue;
};
match method_map
.is_binding_reachable(db, &attribute_assignment)
.binding_reachability(db, &attribute_assignment)
.and(is_method_reachable)
{
Truthiness::AlwaysTrue | Truthiness::Ambiguous => {
Truthiness::AlwaysTrue => {
is_attribute_bound = true;
}
Truthiness::Ambiguous => {
is_attribute_bound = true;
qualifiers |= TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE;
}
Truthiness::AlwaysFalse => {
continue;
Expand All @@ -2834,7 +2841,7 @@ impl<'db> ClassLiteral<'db> {
// TODO: this is incomplete logic since the attributes bound after termination are considered reachable.
let unbound_reachability = unbound_binding
.as_ref()
.map(|binding| method_map.is_binding_reachable(db, binding))
.map(|binding| method_map.binding_reachability(db, binding))
.unwrap_or(Truthiness::AlwaysFalse);

if unbound_reachability
Expand Down
Loading
Loading