mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-17 02:53:01 +00:00
## Summary * Completely removes the concept of visibility constraints. Reachability constraints are now used to model the static visibility of bindings and declarations. Reachability constraints are *much* easier to reason about / work with, since they are applied at the beginning of a branch, and not applied retroactively. Removing the duplication between visibility and reachability constraints also leads to major code simplifications [^1]. For an overview of how the new constraint system works, see the updated doc comment in `reachability_constraints.rs`. * Fixes a [control-flow modeling bug (panic)](https://github.com/astral-sh/ty/issues/365) involving `break` statements in loops * Fixes a [bug where](https://github.com/astral-sh/ty/issues/624) where `elif` branches would have wrong reachability constraints * Fixes a [bug where](https://github.com/astral-sh/ty/issues/648) code after infinite loops would not be considered unreachble * Fixes a panic on the `pywin32` ecosystem project, which we should be able to move to `good.txt` once this has been merged. * Removes some false positives in unreachable code because we infer `Never` more often, due to the fact that reachability constraints now apply retroactively to *all* active bindings, not just to bindings inside a branch. * As one example, this removes the `division-by-zero` diagnostic from https://github.com/astral-sh/ty/issues/443 because we now infer `Never` for the divisor. * Supersedes and includes similar test changes as https://github.com/astral-sh/ruff/pull/18392 closes https://github.com/astral-sh/ty/issues/365 closes https://github.com/astral-sh/ty/issues/624 closes https://github.com/astral-sh/ty/issues/642 closes https://github.com/astral-sh/ty/issues/648 ## Benchmarks Benchmarks on black, pandas, and sympy showed that this is neither a performance improvement, nor a regression. ## Test Plan Regression tests for: - [x] https://github.com/astral-sh/ty/issues/365 - [x] https://github.com/astral-sh/ty/issues/624 - [x] https://github.com/astral-sh/ty/issues/642 - [x] https://github.com/astral-sh/ty/issues/648 [^1]: I'm afraid this is something that @carljm advocated for since the beginning, and I'm not sure anymore why we have never seriously tried this before. So I suggest we do *not* attempt to do a historical deep dive to find out exactly why this ever became so complicated, and just enjoy the fact that we eventually arrived here. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
173 lines
6.1 KiB
Rust
173 lines
6.1 KiB
Rust
//! _Predicates_ are Python expressions whose runtime values can affect type inference.
|
|
//!
|
|
//! We currently use predicates in two places:
|
|
//!
|
|
//! - [_Narrowing constraints_][crate::semantic_index::narrowing_constraints] constrain the type of
|
|
//! a binding that is visible at a particular use.
|
|
//! - [_Reachability constraints_][crate::semantic_index::reachability_constraints] determine the
|
|
//! static reachability of a binding, and the reachability of a statement or expression.
|
|
|
|
use ruff_db::files::File;
|
|
use ruff_index::{IndexVec, newtype_index};
|
|
use ruff_python_ast::Singleton;
|
|
|
|
use crate::db::Db;
|
|
use crate::semantic_index::expression::Expression;
|
|
use crate::semantic_index::global_scope;
|
|
use crate::semantic_index::place::{FileScopeId, ScopeId, ScopedPlaceId};
|
|
|
|
// A scoped identifier for each `Predicate` in a scope.
|
|
#[newtype_index]
|
|
#[derive(Ord, PartialOrd)]
|
|
pub(crate) struct ScopedPredicateId;
|
|
|
|
// A collection of predicates for a given scope.
|
|
pub(crate) type Predicates<'db> = IndexVec<ScopedPredicateId, Predicate<'db>>;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct PredicatesBuilder<'db> {
|
|
predicates: IndexVec<ScopedPredicateId, Predicate<'db>>,
|
|
}
|
|
|
|
impl<'db> PredicatesBuilder<'db> {
|
|
/// Adds a predicate. Note that we do not deduplicate predicates. If you add a `Predicate`
|
|
/// more than once, you will get distinct `ScopedPredicateId`s for each one. (This lets you
|
|
/// model predicates that might evaluate to different values at different points of execution.)
|
|
pub(crate) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
|
|
self.predicates.push(predicate)
|
|
}
|
|
|
|
pub(crate) fn build(mut self) -> Predicates<'db> {
|
|
self.predicates.shrink_to_fit();
|
|
self.predicates
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
|
pub(crate) struct Predicate<'db> {
|
|
pub(crate) node: PredicateNode<'db>,
|
|
pub(crate) is_positive: bool,
|
|
}
|
|
|
|
impl Predicate<'_> {
|
|
pub(crate) fn negated(self) -> Self {
|
|
Self {
|
|
node: self.node,
|
|
is_positive: !self.is_positive,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
|
pub(crate) enum PredicateNode<'db> {
|
|
Expression(Expression<'db>),
|
|
Pattern(PatternPredicate<'db>),
|
|
StarImportPlaceholder(StarImportPlaceholderPredicate<'db>),
|
|
}
|
|
|
|
/// Pattern kinds for which we support type narrowing and/or static reachability analysis.
|
|
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
|
|
pub(crate) enum PatternPredicateKind<'db> {
|
|
Singleton(Singleton),
|
|
Value(Expression<'db>),
|
|
Or(Vec<PatternPredicateKind<'db>>),
|
|
Class(Expression<'db>),
|
|
Unsupported,
|
|
}
|
|
|
|
#[salsa::tracked(debug)]
|
|
pub(crate) struct PatternPredicate<'db> {
|
|
pub(crate) file: File,
|
|
|
|
pub(crate) file_scope: FileScopeId,
|
|
|
|
pub(crate) subject: Expression<'db>,
|
|
|
|
#[returns(ref)]
|
|
pub(crate) kind: PatternPredicateKind<'db>,
|
|
|
|
pub(crate) guard: Option<Expression<'db>>,
|
|
|
|
count: countme::Count<PatternPredicate<'static>>,
|
|
}
|
|
|
|
impl<'db> PatternPredicate<'db> {
|
|
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
|
self.file_scope(db).to_scope_id(db, self.file(db))
|
|
}
|
|
}
|
|
|
|
/// A "placeholder predicate" that is used to model the fact that the boundness of a
|
|
/// (possible) definition or declaration caused by a `*` import cannot be fully determined
|
|
/// until type-inference time. This is essentially the same as a standard reachability constraint,
|
|
/// so we reuse the [`Predicate`] infrastructure to model it.
|
|
///
|
|
/// To illustrate, say we have a module `exporter.py` like so:
|
|
///
|
|
/// ```py
|
|
/// if <condition>:
|
|
/// class A: ...
|
|
/// ```
|
|
///
|
|
/// and we have a module `importer.py` like so:
|
|
///
|
|
/// ```py
|
|
/// A = 1
|
|
///
|
|
/// from importer import *
|
|
/// ```
|
|
///
|
|
/// Since we cannot know whether or not <condition> is true at semantic-index time,
|
|
/// we record a definition for `A` in `b.py` as a result of the `from a import *`
|
|
/// statement, but place a predicate on it to record the fact that we don't yet
|
|
/// know whether this definition will be visible from all control-flow paths or not.
|
|
/// Essentially, we model `b.py` as something similar to this:
|
|
///
|
|
/// ```py
|
|
/// A = 1
|
|
///
|
|
/// if <star_import_placeholder_predicate>:
|
|
/// from a import A
|
|
/// ```
|
|
///
|
|
/// At type-check time, the placeholder predicate for the `A` definition is evaluated by
|
|
/// attempting to resolve the `A` symbol in `a.py`'s global namespace:
|
|
/// - If it resolves to a definitely bound symbol, then the predicate resolves to [`Truthiness::AlwaysTrue`]
|
|
/// - If it resolves to an unbound symbol, then the predicate resolves to [`Truthiness::AlwaysFalse`]
|
|
/// - If it resolves to a possibly bound symbol, then the predicate resolves to [`Truthiness::Ambiguous`]
|
|
///
|
|
/// [Truthiness]: [crate::types::Truthiness]
|
|
#[salsa::tracked(debug)]
|
|
pub(crate) struct StarImportPlaceholderPredicate<'db> {
|
|
pub(crate) importing_file: File,
|
|
|
|
/// Each symbol imported by a `*` import has a separate predicate associated with it:
|
|
/// this field identifies which symbol that is.
|
|
///
|
|
/// Note that a [`ScopedPlaceId`] is only meaningful if you also know the scope
|
|
/// it is relative to. For this specific struct, however, there's no need to store a
|
|
/// separate field to hold the ID of the scope. `StarImportPredicate`s are only created
|
|
/// for valid `*`-import definitions, and valid `*`-import definitions can only ever
|
|
/// exist in the global scope; thus, we know that the `symbol_id` here will be relative
|
|
/// to the global scope of the importing file.
|
|
pub(crate) symbol_id: ScopedPlaceId,
|
|
|
|
pub(crate) referenced_file: File,
|
|
}
|
|
|
|
impl<'db> StarImportPlaceholderPredicate<'db> {
|
|
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
|
// See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]:
|
|
// valid `*`-import definitions can only take place in the global scope.
|
|
global_scope(db, self.importing_file(db))
|
|
}
|
|
}
|
|
|
|
impl<'db> From<StarImportPlaceholderPredicate<'db>> for Predicate<'db> {
|
|
fn from(predicate: StarImportPlaceholderPredicate<'db>) -> Self {
|
|
Predicate {
|
|
node: PredicateNode::StarImportPlaceholder(predicate),
|
|
is_positive: true,
|
|
}
|
|
}
|
|
}
|