ex_unit.Case.isChild and For.treeWalkDown close a cycle on the same walk. Deciding whether a test generated inside a for comprehension is an ExUnit.Case child resolves that call (ModuleWalker.resolvesTo → resolvesToModularName), and resolving it walks back up into the same for and down into its children again — psi/scope/CallDefinitionClause.kt:121 and :160.
Both hinges guard on a ResolveState visited set and neither can see across the boundary between them: resolvesToModularName re-enters through PsiPolyVariantReference.multiResolve(Boolean), which carries no state, and the far side restarts at call_definition_clause.MultiResolve.resolveResults, defaulting to ResolveState.initial(). resolveResults seeds that fresh state with putInitialVisitedElement(entrance), which suffices while the for body holds one generated test. With two, the walk reaches the sibling, which is on no visited set, and the cycle closes. Worth knowing before testing any of this family: a single-test fixture terminates and looks exactly like a fix.
RecursionManager.doPreventingRecursion at reference/resolver/Callable.kt:119 stops it overflowing, being keyed on element identity rather than on a ResolveState. But it stops it by abandoning the computation — doPreventingRecursion returns null, the plugin maps that to emptyList(), and mayCacheNow() is false — so the call resolves to nothing, with no navigation, completion or highlighting, and the walk is redone per request. RecursionManager's KDoc treats arriving here as a defect to fix.
The fix probably carries the visited set across the reference boundary. Unlike #3978 the state cannot simply be threaded, because that boundary is a platform interface; reference.kt already solves this shape for resolvesToMacro with a ThreadLocal<MutableSet<Call>> beside the function. ModuleWalker.resolvesTo is protected and used by ecto/Query, ecto/Schema, ex_unit/Assertions and ex_unit/Case, so a guard there covers all four; watch for a legitimate nested test/describe/from/schema no longer resolving.
Two related issues, both reached the same day. #3405 is this same cycle and is closed — it reported the StackOverflowError and the freeze, fixed in v23.2.0 by the guard above; this is the part that guard does not address. #3978 shares only the root-cause shape: it is a cyclic alias chain in psi/scope/module/MultiResolve.kt, introduced 2026-04-28, and it still overflows in production, whereas this one is contained.
A regression test for the contained behaviour is on the missing-tests branch (#3933), tests/org/elixir_lang/reference/callable/Issue3405Test.kt. It pins the containment, not the residue described here.
Filed as part of an AI-assisted review of the backlog. A human checked this one before it was filed, but it might still be wrong — if any of it is, please say so here.
ex_unit.Case.isChildandFor.treeWalkDownclose a cycle on the same walk. Deciding whether atestgenerated inside aforcomprehension is anExUnit.Casechild resolves that call (ModuleWalker.resolvesTo→resolvesToModularName), and resolving it walks back up into the sameforand down into its children again —psi/scope/CallDefinitionClause.kt:121and:160.Both hinges guard on a
ResolveStatevisited set and neither can see across the boundary between them:resolvesToModularNamere-enters throughPsiPolyVariantReference.multiResolve(Boolean), which carries no state, and the far side restarts atcall_definition_clause.MultiResolve.resolveResults, defaulting toResolveState.initial().resolveResultsseeds that fresh state withputInitialVisitedElement(entrance), which suffices while theforbody holds one generatedtest. With two, the walk reaches the sibling, which is on no visited set, and the cycle closes. Worth knowing before testing any of this family: a single-testfixture terminates and looks exactly like a fix.RecursionManager.doPreventingRecursionatreference/resolver/Callable.kt:119stops it overflowing, being keyed on element identity rather than on aResolveState. But it stops it by abandoning the computation —doPreventingRecursionreturnsnull, the plugin maps that toemptyList(), andmayCacheNow()is false — so the call resolves to nothing, with no navigation, completion or highlighting, and the walk is redone per request.RecursionManager's KDoc treats arriving here as a defect to fix.The fix probably carries the visited set across the reference boundary. Unlike #3978 the state cannot simply be threaded, because that boundary is a platform interface;
reference.ktalready solves this shape forresolvesToMacrowith aThreadLocal<MutableSet<Call>>beside the function.ModuleWalker.resolvesToisprotectedand used byecto/Query,ecto/Schema,ex_unit/Assertionsandex_unit/Case, so a guard there covers all four; watch for a legitimate nestedtest/describe/from/schemano longer resolving.Two related issues, both reached the same day. #3405 is this same cycle and is closed — it reported the
StackOverflowErrorand the freeze, fixed in v23.2.0 by the guard above; this is the part that guard does not address. #3978 shares only the root-cause shape: it is a cyclicaliaschain inpsi/scope/module/MultiResolve.kt, introduced 2026-04-28, and it still overflows in production, whereas this one is contained.A regression test for the contained behaviour is on the
missing-testsbranch (#3933),tests/org/elixir_lang/reference/callable/Issue3405Test.kt. It pins the containment, not the residue described here.Filed as part of an AI-assisted review of the backlog. A human checked this one before it was filed, but it might still be wrong — if any of it is, please say so here.