From ee9634863438da4409d08408a98b1809c45b8e53 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 20 Mar 2026 12:00:06 -0700 Subject: [PATCH] Fix StackOverflowError in Kotlin parser for deeply nested binary expressions Convert recursive left-child traversal in visitBinaryExpression to an iterative approach, collecting the left spine of nested KtBinaryExpression nodes and processing them bottom-up. This prevents stack overflow when parsing deeply nested expressions (e.g. 2000+ string concatenations). --- .../internal/KotlinTreeParserVisitor.java | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/internal/KotlinTreeParserVisitor.java b/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/internal/KotlinTreeParserVisitor.java index ed8e10c0c22..c81748f4346 100644 --- a/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/internal/KotlinTreeParserVisitor.java +++ b/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/internal/KotlinTreeParserVisitor.java @@ -1866,15 +1866,39 @@ public J visitArgument(KtValueArgument argument, ExecutionContext data) { @Override public J visitBinaryExpression(KtBinaryExpression expression, ExecutionContext data) { - assert expression.getLeft() != null; - assert expression.getRight() != null; + // Collect left-spine of nested binary expressions to process iteratively, + // avoiding StackOverflowError on deeply nested expressions (e.g. 2000+ concatenations) + List spine = new ArrayList<>(); + KtExpression leftmost = expression; + while (leftmost instanceof KtBinaryExpression) { + spine.add((KtBinaryExpression) leftmost); + leftmost = ((KtBinaryExpression) leftmost).getLeft(); + } + + // Visit the deepest left operand (not a binary expression) + assert leftmost != null; + J leftJ = leftmost.accept(this, data); + + // Process each binary expression from innermost to outermost + J result = null; + for (int i = spine.size() - 1; i >= 0; i--) { + KtBinaryExpression binExpr = spine.get(i); + assert binExpr.getRight() != null; + + Expression left = convertToExpression(leftJ).withPrefix(Space.EMPTY); + result = visitSingleBinaryExpression(binExpr, left, data); + leftJ = result; + } + + return result; + } + private J visitSingleBinaryExpression(KtBinaryExpression expression, Expression left, ExecutionContext data) { KtOperationReferenceExpression operationReference = expression.getOperationReference(); J.Binary.Type javaBinaryType = mapJBinaryType(operationReference); J.AssignmentOperation.Type assignmentOperationType = javaBinaryType == null ? mapAssignmentOperationType(operationReference) : null; K.Binary.Type kotlinBinaryType = javaBinaryType == null && assignmentOperationType == null ? mapKBinaryType(operationReference) : null; - Expression left = convertToExpression(expression.getLeft().accept(this, data)).withPrefix(Space.EMPTY); Expression right = convertToExpression((expression.getRight()).accept(this, data)) .withPrefix(prefix(expression.getRight())); JavaType type = type(expression); @@ -1921,7 +1945,7 @@ public J visitBinaryExpression(KtBinaryExpression expression, ExecutionContext d )); } - return mapFunctionCall(expression, data); + return mapFunctionCall(expression, left, data); } private J.AssignmentOperation.@Nullable Type mapAssignmentOperationType(KtOperationReferenceExpression operationReference) { @@ -3398,12 +3422,12 @@ else if (elementType == KtTokens.OROR) } } - private J.MethodInvocation mapFunctionCall(KtBinaryExpression expression, ExecutionContext data) { + private J.MethodInvocation mapFunctionCall(KtBinaryExpression expression, Expression precomputedLeft, ExecutionContext data) { Markers markers = Markers.EMPTY .addIfAbsent(new Infix(randomId())) .addIfAbsent(new Extension(randomId())); - Expression selectExp = convertToExpression(requireNonNull(expression.getLeft()).accept(this, data).withPrefix(prefix(expression.getLeft()))); + Expression selectExp = precomputedLeft.withPrefix(prefix(expression.getLeft())); JRightPadded select = padRight(selectExp, Space.EMPTY); J.Identifier name = (J.Identifier) expression.getOperationReference().accept(this, data); // createIdentifier(operation, Space.EMPTY, methodInvocationType(expression));