openrewrite / openrewrite/rewrite-analysis
StackOverflowError: unbounded mutual recursion between the argument→select and select→argument steps in ForwardFlow.computeVariableAssignment
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 16
- Forks
- 12
- Avg merge
- 2h 26m
- Merged PRs (30d)
- 1
Description
What version of OpenRewrite are you using?
Reproduced against org.openrewrite.meta:rewrite-analysis:2.38.0-SNAPSHOT (the stack trace line numbers below match that build exactly).
How are you running OpenRewrite?
Via org.openrewrite.staticanalysis.ReplaceStackWithDeque, which uses FindLocalFlowPaths with a plain DataFlowSpec.
What is the smallest, simplest way to reproduce the problem?
import java.util.Optional;
import java.util.Stack;
class Test {
void test(Optional<String> opt) {
Stack<Integer> stack = new Stack<>();
String a = "x";
String s = Optional.ofNullable(a).orElse(opt.get());
}
}
(The Stack is only there to satisfy the recipe's UsesType precondition; the overflow happens while computing flow out of a.)
What did you expect to see?
The recipe completes.
What did you see instead?
java.lang.StackOverflowError, with ~1000 frames alternating between exactly two call sites in ForwardFlow.computeVariableAssignment:
java.lang.StackOverflowError
...
org.openrewrite.Cursor.getParentTreeCursor(Cursor.java:253)
org.openrewrite.analysis.trait.expr.VarAccessBase.viewOf(VarAccess.java:221)
org.openrewrite.analysis.trait.expr.VarAccess$Factory.viewOf(VarAccess.java:86)
org.openrewrite.analysis.trait.TraitFactory.findFirstViewOf(TraitFactory.java:87)
org.openrewrite.analysis.trait.TraitFactory.lambda$findFirstViewOf$2(TraitFactory.java:96)
fj.data.Validation$FailProjection.bind(Validation.java:1249)
org.openrewrite.analysis.trait.TraitFactory.findFirstViewOf(TraitFactory.java:95)
org.openrewrite.analysis.trait.TraitFactory.findFirstViewOf(TraitFactory.java:69)
org.openrewrite.analysis.trait.expr.Expr$Factory.viewOf(Expr.java:37)
org.openrewrite.analysis.trait.expr.Expr.viewOf(Expr.java:51)
org.openrewrite.analysis.dataflow.DataFlowNode.of(DataFlowNode.java:63)
org.openrewrite.analysis.dataflow.analysis.ForwardFlow.computeVariableAssignment(ForwardFlow.java:458)
org.openrewrite.analysis.dataflow.analysis.ForwardFlow.computeVariableAssignment(ForwardFlow.java:495)
org.openrewrite.analysis.dataflow.analysis.ForwardFlow.computeVariableAssignment(ForwardFlow.java:466)
org.openrewrite.analysis.dataflow.analysis.ForwardFlow.computeVariableAssignment(ForwardFlow.java:495)
org.openrewrite.analysis.dataflow.analysis.ForwardFlow.computeVariableAssignment(ForwardFlow.java:466)
... (502 frames at :466, 501 frames at :495)
Root cause
computeVariableAssignment has two mutually recursive descents into siblings of the same J.MethodInvocation, with no visited set and no depth bound:
ForwardFlow.java:455-472— "flow from an argument to the select", recursing on the select cursor (line 466).ForwardFlow.java:477-503— "flow from the select (or another argument) to an argument", recursing on the argument cursor (line 495).
If spec.isFlowStep(..) holds in both directions for one call, the two branches call each other forever: arg -> select -> arg -> select -> ....
For Optional.ofNullable(a).orElse(opt.get()) both directions hold, and neither comes from a bidirectional model — they come from two different ReturnValue model groups that Optimizer.optimize merges into name-indexed union matchers:
flowFromArgumentIndexToReturn[0](models withArgument[0] -> ReturnValue, which includesOptional.orElseandOptional.ofNullable). Its predicate issinkNode is a Call matching M && isParameter(srcNode, 0). With src =opt.get()(argument 0 oforElse) and sink =Optional.ofNullable(a)(aCall, andofNullableis in the group) this is a flow step — so thearg -> selectbranch fires.flowFromArgumentIndexToReturn[-1](models withArgument[this] -> ReturnValue, which includesOptional.orElseandOptional.get). Its predicate issinkNode is a Call matching M && isSelect(srcNode). With src =Optional.ofNullable(a)(the select oforElse) and sink =opt.get()(aCall, andgetis in the group) this is also a flow step — so theselect -> argbranch fires.
So the trigger is: a call whose select is itself a call and whose argument is itself a call, where the outer method appears in both the Argument[k] -> ReturnValue and Argument[this] -> ReturnValue value-model groups. Optional.orElse is one such method; there are others.
The forFlowFromArgumentIndexToQualifier optimizer already asserts "Argument[-1] is the 'select' or 'qualifier' of a method call. Flow would be cyclic.", so the cycle hazard is known — it just isn't guarded on this path.
Where this shows up in the wild
org.openrewrite.staticanalysis.ReplaceStackWithDeque on spring-projects/spring-data-mongodb, file
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java.
Recurring since at least 2026-08-17 in the "Java best practices" and "Common static analysis issues" flagship runs on dev, against ALL, Open Source, Netflix + Spring + Apache and Netflix + Spring. Confirmed present in run 20260825041417-SSuzc (org.openrewrite.table.SourcesFileErrors names org.openrewrite.staticanalysis.ReplaceStackWithDeque / java.lang.StackOverflowError with the trace above) and absent on 2026-08-26 — consistent with the trigger depending on which expressions in the file get full type attribution in a given LST build, not on anything about the Stack itself.
Suggested fix
Guard the mutual recursion — e.g. carry a set of already-visited cursors (or the FlowGraph nodes already on the path) through computeVariableAssignment and skip a recursive descent into a node that is already on the current path.
Note that a caller cannot work around this: DataFlowSpec.isFlowStep is final, so a spec cannot suppress the external models that create the cycle.
/cc a recipe-side mitigation is in openrewrite/rewrite-static-analysis (only run the analysis for variables the recipe can actually rewrite), but that only narrows the blast radius; any spec whose source flows into such an expression still overflows.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/main/java/org/openrewrite/analysis/dataflow/analysis/ForwardFlow.java, especially computeVariableAssignment around lines 455-503, and reproduce with the Optional.ofNullable(a).orElse(opt.get()) example through ReplaceStackWithDeque. Trace the argument-to-select and select-to-argument descents and add regression coverage showing the analysis terminates without StackOverflowError. Done means the minimal reproduction and the reported MongoExampleMapper case no longer overflow.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100