mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-28 23:43:53 +00:00

Some checks are pending
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
## Summary This changeset adds support for precise type-inference and boundness-handling of definitions inside control-flow branches with statically-known conditions, i.e. test-expressions whose truthiness we can unambiguously infer as *always false* or *always true*. This branch also includes: - `sys.platform` support - statically-known branches handling for Boolean expressions and while loops - new `target-version` requirements in some Markdown tests which were now required due to the understanding of `sys.version_info` branches. closes #12700 closes #15034 ## Performance ### `tomllib`, -7%, needs to resolve one additional module (sys) | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `./red_knot_main --project /home/shark/tomllib` | 22.2 ± 1.3 | 19.1 | 25.6 | 1.00 | | `./red_knot_feature --project /home/shark/tomllib` | 23.8 ± 1.6 | 20.8 | 28.6 | 1.07 ± 0.09 | ### `black`, -6% | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `./red_knot_main --project /home/shark/black` | 129.3 ± 5.1 | 119.0 | 137.8 | 1.00 | | `./red_knot_feature --project /home/shark/black` | 136.5 ± 6.8 | 123.8 | 147.5 | 1.06 ± 0.07 | ## Test Plan - New Markdown tests for the main feature in `statically-known-branches.md` - New Markdown tests for `sys.platform` - Adapted tests for `EllipsisType`, `Never`, etc
146 lines
4.6 KiB
Rust
146 lines
4.6 KiB
Rust
use crate::{
|
|
types::{Type, UnionType},
|
|
Db,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum Boundness {
|
|
Bound,
|
|
PossiblyUnbound,
|
|
}
|
|
|
|
impl Boundness {
|
|
pub(crate) fn or(self, other: Boundness) -> Boundness {
|
|
match (self, other) {
|
|
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
|
|
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The result of a symbol lookup, which can either be a (possibly unbound) type
|
|
/// or a completely unbound symbol.
|
|
///
|
|
/// Consider this example:
|
|
/// ```py
|
|
/// bound = 1
|
|
///
|
|
/// if flag:
|
|
/// possibly_unbound = 2
|
|
/// ```
|
|
///
|
|
/// If we look up symbols in this scope, we would get the following results:
|
|
/// ```rs
|
|
/// bound: Symbol::Type(Type::IntLiteral(1), Boundness::Bound),
|
|
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
|
|
/// non_existent: Symbol::Unbound,
|
|
/// ```
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) enum Symbol<'db> {
|
|
Type(Type<'db>, Boundness),
|
|
Unbound,
|
|
}
|
|
|
|
impl<'db> Symbol<'db> {
|
|
pub(crate) fn is_unbound(&self) -> bool {
|
|
matches!(self, Symbol::Unbound)
|
|
}
|
|
|
|
pub(crate) fn possibly_unbound(&self) -> bool {
|
|
match self {
|
|
Symbol::Type(_, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
|
|
Symbol::Type(_, Boundness::Bound) => false,
|
|
}
|
|
}
|
|
|
|
/// Returns the type of the symbol, ignoring possible unboundness.
|
|
///
|
|
/// If the symbol is *definitely* unbound, this function will return `None`. Otherwise,
|
|
/// if there is at least one control-flow path where the symbol is bound, return the type.
|
|
pub(crate) fn ignore_possibly_unbound(&self) -> Option<Type<'db>> {
|
|
match self {
|
|
Symbol::Type(ty, _) => Some(*ty),
|
|
Symbol::Unbound => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[track_caller]
|
|
pub(crate) fn expect_type(self) -> Type<'db> {
|
|
self.ignore_possibly_unbound()
|
|
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
|
}
|
|
|
|
#[must_use]
|
|
pub(crate) fn or_fall_back_to(self, db: &'db dyn Db, fallback: &Symbol<'db>) -> Symbol<'db> {
|
|
match fallback {
|
|
Symbol::Type(fallback_ty, fallback_boundness) => match self {
|
|
Symbol::Type(_, Boundness::Bound) => self,
|
|
Symbol::Type(ty, boundness @ Boundness::PossiblyUnbound) => Symbol::Type(
|
|
UnionType::from_elements(db, [*fallback_ty, ty]),
|
|
fallback_boundness.or(boundness),
|
|
),
|
|
Symbol::Unbound => fallback.clone(),
|
|
},
|
|
Symbol::Unbound => self,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::db::tests::setup_db;
|
|
|
|
#[test]
|
|
fn test_symbol_or_fall_back_to() {
|
|
use Boundness::{Bound, PossiblyUnbound};
|
|
|
|
let db = setup_db();
|
|
let ty1 = Type::IntLiteral(1);
|
|
let ty2 = Type::IntLiteral(2);
|
|
|
|
// Start from an unbound symbol
|
|
assert_eq!(
|
|
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Unbound),
|
|
Symbol::Unbound
|
|
);
|
|
assert_eq!(
|
|
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, PossiblyUnbound)),
|
|
Symbol::Type(ty1, PossiblyUnbound)
|
|
);
|
|
assert_eq!(
|
|
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, Bound)),
|
|
Symbol::Type(ty1, Bound)
|
|
);
|
|
|
|
// Start from a possibly unbound symbol
|
|
assert_eq!(
|
|
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Unbound),
|
|
Symbol::Type(ty1, PossiblyUnbound)
|
|
);
|
|
assert_eq!(
|
|
Symbol::Type(ty1, PossiblyUnbound)
|
|
.or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
|
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), PossiblyUnbound)
|
|
);
|
|
assert_eq!(
|
|
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
|
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), Bound)
|
|
);
|
|
|
|
// Start from a definitely bound symbol
|
|
assert_eq!(
|
|
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Unbound),
|
|
Symbol::Type(ty1, Bound)
|
|
);
|
|
assert_eq!(
|
|
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
|
Symbol::Type(ty1, Bound)
|
|
);
|
|
assert_eq!(
|
|
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
|
Symbol::Type(ty1, Bound)
|
|
);
|
|
}
|
|
}
|