[ty] remove static_expression_truthiness and improve reachability analysis (#22971)

## Summary

cf: https://github.com/astral-sh/ruff/pull/19579,
https://github.com/astral-sh/ruff/pull/20566#discussion_r2553196955

Currently, we use the query `static_expression_truthiness` to determine
the truthiness of an expression.
This always determines the truthinesses of non-definitely-bound places
as `Ambiguous`, preventing cycles that occur when their reachabilities
depend on themselves. However, this can lead to inaccurate analysis of
code like the following.

```python
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 ALWAYS_TRUE_IF_BOUND:
        x = 1
    else:
        x = 2

    # If `ALWAYS_TRUE_IF_BOUND` were not defined, an error would occur, and therefore the `x = 2` branch would never be executed.
    reveal_type(x)  # expected: Literal[1], but: Literal[1, 2]
```

Since #20566, we can determine the `Place` by looking at the previous
cycle result.
Therefore, we can now remove `static_expression_truthiness`, improving
reachability analysis of the above code.

## Test Plan

mdtest updated
This commit is contained in:
Shunsuke Shibayama
2026-01-31 08:30:11 +09:00
committed by GitHub
parent 9ebdd85d6d
commit 48a4d9d706
5 changed files with 25 additions and 40 deletions
@@ -2708,8 +2708,9 @@ class Toggle:
if check(self.y):
self.y = True
reveal_type(Toggle().x) # revealed: Literal[True]
reveal_type(Toggle().y) # revealed: Unknown | Literal[True]
# Literal[True] or undefined
reveal_type(Toggle().x) # revealed: Literal[True] | Unknown
reveal_type(Toggle().y) # revealed: Unknown | Literal[True]
```
Make sure that the growing union of literals `Literal[0, 1, 2, ...]` collapses to `int` during
@@ -1578,10 +1578,25 @@ def _(flag: bool):
if True and ALWAYS_TRUE_IF_BOUND:
x = 1
# error: [possibly-unresolved-reference] "Name `x` used when possibly not defined"
# no error, x is considered definitely bound
x
```
```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
else:
x = 2
# If `ALWAYS_TRUE_IF_BOUND` were not defined, an error would occur, and therefore the `x = 2` branch would never be executed.
reveal_type(x) # revealed: Literal[1]
```
## Unreachable code
A closely related feature is the ability to detect unreachable code. For example, we do not emit a
@@ -209,7 +209,7 @@ use crate::semantic_index::predicate::{
};
use crate::types::{
CallableTypes, IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
infer_expression_type, static_expression_truthiness,
infer_expression_type,
};
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
@@ -864,7 +864,9 @@ impl ReachabilityConstraints {
match predicate.node {
PredicateNode::Expression(test_expr) => {
static_expression_truthiness(db, test_expr).negate_if(!predicate.is_positive)
infer_expression_type(db, test_expr, TypeContext::default())
.bool(db)
.negate_if(!predicate.is_positive)
}
PredicateNode::ReturnsNever(CallableAndCallExpr {
callable,
+1 -1
View File
@@ -32,7 +32,7 @@ pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
pub(crate) use self::infer::{
TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types,
infer_expression_type, infer_expression_types, infer_scope_types, static_expression_truthiness,
infer_expression_type, infer_expression_types, infer_scope_types,
};
pub use self::signatures::ParameterKind;
pub(crate) use self::signatures::{CallableSignature, Signature};
+1 -34
View File
@@ -53,8 +53,7 @@ use crate::types::function::FunctionType;
use crate::types::generics::Specialization;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
ClassLiteral, KnownClass, StaticClassLiteral, Truthiness, Type, TypeAndQualifiers,
declaration_type,
ClassLiteral, KnownClass, StaticClassLiteral, Type, TypeAndQualifiers, declaration_type,
};
use crate::unpack::Unpack;
use builder::TypeInferenceBuilder;
@@ -428,30 +427,6 @@ impl<'db> TypeContext<'db> {
}
}
/// Returns the statically-known truthiness of a given expression.
///
/// Returns [`Truthiness::Ambiguous`] in case any non-definitely bound places
/// were encountered while inferring the type of the expression.
#[salsa::tracked(
cycle_initial=|_, _, _| Truthiness::Ambiguous,
heap_size=get_size2::GetSize::get_heap_size
)]
pub(crate) fn static_expression_truthiness<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Truthiness {
let inference = infer_expression_types_impl(db, InferExpression::Bare(expression));
if !inference.all_places_definitely_bound() {
return Truthiness::Ambiguous;
}
let file = expression.file(db);
let module = parsed_module(db, file).load(db);
let node = expression.node_ref(db, &module);
inference.expression_type(node).bool(db)
}
/// Infer the types for an [`Unpack`] operation.
///
/// This infers the expression type and performs structural match against the target expression
@@ -905,12 +880,4 @@ impl<'db> ExpressionInference<'db> {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra.as_ref().and_then(|extra| extra.cycle_recovery)
}
/// Returns true if all places in this expression are definitely bound.
pub(crate) fn all_places_definitely_bound(&self) -> bool {
self.extra
.as_ref()
.map(|e| e.all_definitely_bound)
.unwrap_or(true)
}
}