mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 22:54:42 +00:00
[ty] Improve subscript narrowing for "safe mutable classes" (#19781)
Some checks are pending
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 (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (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 / Fuzz for new ty panics (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 / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
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 (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (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 / Fuzz for new ty panics (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 / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary This PR improves the `is_safe_mutable_class` function in `infer.rs` in several ways: - It uses `KnownClass::to_instance()` for all "safe mutable classes". Previously, we were using `SpecialFormType::instance_fallback()` for some variants -- I'm not totally sure why. Switching to `KnownClass::to_instance()` for all "safe mutable classes" fixes a number of TODOs in the `assignment.md` mdtest suite - Rather than eagerly calling `.to_instance(db)` on all "safe mutable classes" every time `is_safe_mutable_class` is called, we now only call it lazily on each element, allowing us to short-circuit more effectively. - I removed the entry entirely for `TypedDict` from the list of "safe mutable classes", as it's not correct. `SpecialFormType::TypedDict.instance_fallback(db)` just returns an instance type representing "any instance of `typing._SpecialForm`", which I don't think was the intent of this code. No tests fail as a result of removing this entry, as we already check separately whether an object is an inhabitant of a `TypedDict` type (and consider that object safe-mutable if so!). ## Test Plan mdtests updated
This commit is contained in:
parent
4887bdf205
commit
529d81daca
2 changed files with 37 additions and 37 deletions
|
@ -213,23 +213,20 @@ reveal_type(l[0]) # revealed: Literal[0]
|
||||||
reveal_type(d[0]) # revealed: Literal[0]
|
reveal_type(d[0]) # revealed: Literal[0]
|
||||||
reveal_type(b[0]) # revealed: Literal[0]
|
reveal_type(b[0]) # revealed: Literal[0]
|
||||||
reveal_type(dd[0]) # revealed: Literal[0]
|
reveal_type(dd[0]) # revealed: Literal[0]
|
||||||
# TODO: should be Literal[0]
|
reveal_type(cm[0]) # revealed: Literal[0]
|
||||||
reveal_type(cm[0]) # revealed: Unknown
|
|
||||||
|
|
||||||
class C:
|
class C:
|
||||||
reveal_type(l[0]) # revealed: Literal[0]
|
reveal_type(l[0]) # revealed: Literal[0]
|
||||||
reveal_type(d[0]) # revealed: Literal[0]
|
reveal_type(d[0]) # revealed: Literal[0]
|
||||||
reveal_type(b[0]) # revealed: Literal[0]
|
reveal_type(b[0]) # revealed: Literal[0]
|
||||||
reveal_type(dd[0]) # revealed: Literal[0]
|
reveal_type(dd[0]) # revealed: Literal[0]
|
||||||
# TODO: should be Literal[0]
|
reveal_type(cm[0]) # revealed: Literal[0]
|
||||||
reveal_type(cm[0]) # revealed: Unknown
|
|
||||||
|
|
||||||
[reveal_type(l[0]) for _ in range(1)] # revealed: Literal[0]
|
[reveal_type(l[0]) for _ in range(1)] # revealed: Literal[0]
|
||||||
[reveal_type(d[0]) for _ in range(1)] # revealed: Literal[0]
|
[reveal_type(d[0]) for _ in range(1)] # revealed: Literal[0]
|
||||||
[reveal_type(b[0]) for _ in range(1)] # revealed: Literal[0]
|
[reveal_type(b[0]) for _ in range(1)] # revealed: Literal[0]
|
||||||
[reveal_type(dd[0]) for _ in range(1)] # revealed: Literal[0]
|
[reveal_type(dd[0]) for _ in range(1)] # revealed: Literal[0]
|
||||||
# TODO: should be Literal[0]
|
[reveal_type(cm[0]) for _ in range(1)] # revealed: Literal[0]
|
||||||
[reveal_type(cm[0]) for _ in range(1)] # revealed: Unknown
|
|
||||||
|
|
||||||
def _():
|
def _():
|
||||||
reveal_type(l[0]) # revealed: int | None
|
reveal_type(l[0]) # revealed: int | None
|
||||||
|
|
|
@ -1785,6 +1785,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) {
|
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) {
|
||||||
|
/// Arbitrary `__getitem__`/`__setitem__` methods on a class do not
|
||||||
|
/// necessarily guarantee that the passed-in value for `__setitem__` is stored and
|
||||||
|
/// can be retrieved unmodified via `__getitem__`. Therefore, we currently only
|
||||||
|
/// perform assignment-based narrowing on a few built-in classes (`list`, `dict`,
|
||||||
|
/// `bytesarray`, `TypedDict` and `collections` types) where we are confident that
|
||||||
|
/// this kind of narrowing can be performed soundly. This is the same approach as
|
||||||
|
/// pyright. TODO: Other standard library classes may also be considered safe. Also,
|
||||||
|
/// subclasses of these safe classes that do not override `__getitem__/__setitem__`
|
||||||
|
/// may be considered safe.
|
||||||
|
fn is_safe_mutable_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
|
||||||
|
const SAFE_MUTABLE_CLASSES: &[KnownClass] = &[
|
||||||
|
KnownClass::List,
|
||||||
|
KnownClass::Dict,
|
||||||
|
KnownClass::Bytearray,
|
||||||
|
KnownClass::DefaultDict,
|
||||||
|
KnownClass::ChainMap,
|
||||||
|
KnownClass::Counter,
|
||||||
|
KnownClass::Deque,
|
||||||
|
KnownClass::OrderedDict,
|
||||||
|
];
|
||||||
|
|
||||||
|
SAFE_MUTABLE_CLASSES
|
||||||
|
.iter()
|
||||||
|
.map(|class| class.to_instance(db))
|
||||||
|
.any(|safe_mutable_class| {
|
||||||
|
ty.is_equivalent_to(db, safe_mutable_class)
|
||||||
|
|| ty
|
||||||
|
.generic_origin(db)
|
||||||
|
.zip(safe_mutable_class.generic_origin(db))
|
||||||
|
.is_some_and(|(l, r)| l == r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
binding
|
binding
|
||||||
.kind(self.db())
|
.kind(self.db())
|
||||||
|
@ -2026,38 +2059,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
let value_ty = self
|
let value_ty = self
|
||||||
.try_expression_type(value)
|
.try_expression_type(value)
|
||||||
.unwrap_or_else(|| self.infer_expression(value));
|
.unwrap_or_else(|| self.infer_expression(value));
|
||||||
// Arbitrary `__getitem__`/`__setitem__` methods on a class do not
|
|
||||||
// necessarily guarantee that the passed-in value for `__setitem__` is stored and
|
|
||||||
// can be retrieved unmodified via `__getitem__`. Therefore, we currently only
|
|
||||||
// perform assignment-based narrowing on a few built-in classes (`list`, `dict`,
|
|
||||||
// `bytesarray`, `TypedDict` and `collections` types) where we are confident that
|
|
||||||
// this kind of narrowing can be performed soundly. This is the same approach as
|
|
||||||
// pyright. TODO: Other standard library classes may also be considered safe. Also,
|
|
||||||
// subclasses of these safe classes that do not override `__getitem__/__setitem__`
|
|
||||||
// may be considered safe.
|
|
||||||
let is_safe_mutable_class = || {
|
|
||||||
let safe_mutable_classes = [
|
|
||||||
KnownClass::List.to_instance(db),
|
|
||||||
KnownClass::Dict.to_instance(db),
|
|
||||||
KnownClass::Bytearray.to_instance(db),
|
|
||||||
KnownClass::DefaultDict.to_instance(db),
|
|
||||||
SpecialFormType::ChainMap.instance_fallback(db),
|
|
||||||
SpecialFormType::Counter.instance_fallback(db),
|
|
||||||
SpecialFormType::Deque.instance_fallback(db),
|
|
||||||
SpecialFormType::OrderedDict.instance_fallback(db),
|
|
||||||
SpecialFormType::TypedDict.instance_fallback(db),
|
|
||||||
];
|
|
||||||
|
|
||||||
safe_mutable_classes.iter().any(|safe_mutable_class| {
|
if !value_ty.is_typed_dict() && !is_safe_mutable_class(db, value_ty) {
|
||||||
value_ty.is_equivalent_to(db, *safe_mutable_class)
|
|
||||||
|| value_ty
|
|
||||||
.generic_origin(db)
|
|
||||||
.zip(safe_mutable_class.generic_origin(db))
|
|
||||||
.is_some_and(|(l, r)| l == r)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if !value_ty.is_typed_dict() && !is_safe_mutable_class() {
|
|
||||||
bound_ty = declared_ty;
|
bound_ty = declared_ty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue