Fix an infinite recursion in PyNamedParameterImpl#getTypeFromUsages

Use `context.assumeType` to avoid an infinite recursion while processing local variables and getting the type from usages

See tests `PyTypeTest#testAnyIsNone` and `PyTypeTest.testParameterFromUsages` for an example


(cherry picked from commit c68125b556e264d99a0bd5217827e11a666ca2e4)

IJ-MR-172702

GitOrigin-RevId: b0ee153f1164033aa97195f9a2f015c771ce7c44
This commit is contained in:
evgeny.bovykin
2025-11-19 13:10:40 +00:00
committed by intellij-monorepo-bot
parent 27e5a7690d
commit ccacedc456
2 changed files with 37 additions and 26 deletions
@@ -205,36 +205,43 @@ public class PyNamedParameterImpl extends PyBaseElementImpl<PyNamedParameterStub
}
}
}
// Guess the type from file-local calls
if (context.allowCallContext(this)) {
final List<PyType> types = new ArrayList<>();
final PyResolveContext resolveContext = PyResolveContext.defaultContext(context);
final PyCallableParameter parameter = PyCallableParameterImpl.psi(this);
// Guess the type under an assumed type to prevent recursion
final PyType assumedResult = context.assumeType(this, null, ctx -> {
// Guess the type from file-local calls
if (ctx.allowCallContext(this)) {
final List<PyType> types = new ArrayList<>();
final PyResolveContext resolveContext = PyResolveContext.defaultContext(ctx);
final PyCallableParameter parameter = PyCallableParameterImpl.psi(this);
processLocalCalls(
func, call -> {
StreamEx
.of(call.multiMapArguments(resolveContext))
.flatCollection(mapping -> mapping.getMappedParameters().entrySet())
.filter(entry -> parameter.equals(entry.getValue()))
.map(Map.Entry::getKey)
.nonNull()
.map(context::getType)
.nonNull()
.forEach(types::add);
return true;
processLocalCalls(
func, call -> {
StreamEx
.of(call.multiMapArguments(resolveContext))
.flatCollection(mapping -> mapping.getMappedParameters().entrySet())
.filter(entry -> parameter.equals(entry.getValue()))
.map(Map.Entry::getKey)
.nonNull()
.map(ctx::getType)
.nonNull()
.forEach(types::add);
return true;
}
);
if (!types.isEmpty()) {
return PyUnionType.createWeakType(PyUnionType.union(types));
}
);
if (!types.isEmpty()) {
return PyUnionType.createWeakType(PyUnionType.union(types));
}
}
if (context.maySwitchToAST(this)) {
final PyType typeFromUsages = getTypeFromUsages(context);
if (typeFromUsages != null) {
return typeFromUsages;
if (ctx.maySwitchToAST(this)) {
final PyType typeFromUsages = getTypeFromUsages(ctx);
if (typeFromUsages != null) {
return typeFromUsages;
}
}
return null;
});
if (assumedResult != null) {
return assumedResult;
}
}
}
@@ -2,6 +2,8 @@
package com.jetbrains.python;
import com.google.common.collect.ImmutableList;
import com.intellij.openapi.util.RecursionManager;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.testFramework.LightProjectDescriptor;
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
import com.jetbrains.python.fixtures.PyTestCase;
@@ -783,6 +785,8 @@ public class PyTypeTest extends PyTestCase {
}
public void testParameterFromUsages() {
RecursionManager.assertOnRecursionPrevention(myFixture.getTestRootDisposable());
Registry.get("python.use.better.control.flow.type.inference").setValue(true);
final String text = """
def foo(bar):
expr = bar