From 1945bfdb84b9268a998fd947d3ce9f80c4d55304 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 13:52:08 +0200 Subject: [PATCH] [ty] Fix standalone expression type retrieval in presence of cycles (#17849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When entering an `infer_expression_types` cycle from `TypeInferenceBuilder::infer_standalone_expression`, we might get back a `TypeInference::cycle_fallback(…)` that doesn't actually contain any new types, but instead it contains a `cycle_fallback_type` which is set to `Some(Type::Never)`. When calling `self.extend(…)`, we therefore don't really pull in a type for the expression we're interested in. This caused us to panic if we tried to call `self.expression_type(…)` after `self.extend(…)`. The proposed fix here is to retrieve that type from the nested `TypeInferenceBuilder` directly, which will correctly fall back to `cycle_fallback_type`. ## Details I minimized the second example from #17792 a bit further and used this example for debugging: ```py from __future__ import annotations class C: ... def f(arg: C): pass x, _ = f(1) assert x ``` This is self-referential because when we check the assignment statement `x, _ = f(1)`, we need to look up the signature of `f`. Since evaluation of annotations is deferred, we look up the public type of `C` for the `arg` parameter. The public use of `C` is visibility-constraint by "`x`" via the `assert` statement. While evaluating this constraint, we need to look up the type of `x`, which in turn leads us back to the `x, _ = f(1)` definition. The reason why this only showed up in the relatively peculiar case with unpack assignments is the code here: https://github.com/astral-sh/ruff/blob/78b4c3ccf1d6cb10613671ccec09cafba0d1de72/crates/ty_python_semantic/src/types/infer.rs#L2709-L2718 For a non-unpack assignment like `x = f(1)`, we would not try to infer the right-hand side eagerly. Instead, we would enter a `infer_definition_types` cycle that handles the situation correctly. For unpack assignments, however, we try to infer the type of `value` (`f(1)`) and therefore enter the cycle via `standalone_expression_type => infer_expression_type`. closes #17792 ## Test Plan * New regression test * Made sure that we can now run successfully on scipy => see #17850 --- .../test/corpus/88_regression_issue_17792.py | 15 +++++++++++++++ crates/ty_python_semantic/src/types/infer.rs | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 crates/ty_project/resources/test/corpus/88_regression_issue_17792.py diff --git a/crates/ty_project/resources/test/corpus/88_regression_issue_17792.py b/crates/ty_project/resources/test/corpus/88_regression_issue_17792.py new file mode 100644 index 0000000000..c9977a8397 --- /dev/null +++ b/crates/ty_project/resources/test/corpus/88_regression_issue_17792.py @@ -0,0 +1,15 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/17792 + +from __future__ import annotations + + +class C: ... + + +def f(arg: C): + pass + + +x, _ = f(1) + +assert x diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 6698ffa23a..ba08e40545 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4002,7 +4002,12 @@ impl<'db> TypeInferenceBuilder<'db> { let standalone_expression = self.index.expression(expression); let types = infer_expression_types(self.db(), standalone_expression); self.extend(types); - self.expression_type(expression) + + // Instead of calling `self.expression_type(expr)` after extending here, we get + // the result from `types` directly because we might be in cycle recovery where + // `types.cycle_fallback_type` is `Some(fallback_ty)`, which we can retrieve by + // using `expression_type` on `types`: + types.expression_type(expression.scoped_expression_id(self.db(), self.scope())) } fn infer_expression_impl(&mut self, expression: &ast::Expr) -> Type<'db> {