Skip to content

Commit 75e9711

Browse files
authored
Catch Throwable instead of Exception in KotlinParser and PythonParser (#6633)
* Catch StackOverflowError during Kotlin parsing When parsing Kotlin files with deeply nested binary expressions (e.g., very long string concatenations like "a" + "b" + "c" + ...), the Kotlin compiler's FIR builder uses recursive descent which can exceed the JVM stack limit. Changed the catch block from catching Exception to Throwable so that StackOverflowError is caught and converted to a ParseError instead of crashing the entire parsing process. This allows mass ingest to continue processing other files when one file has this issue. Added a test case that verifies deeply nested concatenations (2000+ operations) result in a ParseError with StackOverflowError rather than crashing. Fixes moderneinc/customer-requests#1694 * Also catch Throwable in PythonParser Apply the same fix to PythonParser for consistency with all other parsers.
1 parent ac001a4 commit 75e9711

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

rewrite-kotlin/src/main/java/org/openrewrite/kotlin/KotlinParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ public Stream<SourceFile> parseInputs(Iterable<Input> sources, @Nullable Path re
169169
List<Input> acceptedInputs = ListUtils.concatAll(dependsOn, acceptedInputs(sources).collect(toList()));
170170
try {
171171
compilerCus = parse(acceptedInputs, disposable, pctx);
172-
} catch (Exception e) {
172+
} catch (Throwable t) {
173173
disposable.dispose();
174-
return acceptedInputs.stream().map(input -> ParseError.build(this, input, relativeTo, ctx, e));
174+
return acceptedInputs.stream().map(input -> ParseError.build(this, input, relativeTo, ctx, t));
175175
}
176176

177177
FirSession firSession = compilerCus.getFirSession();

rewrite-kotlin/src/test/java/org/openrewrite/kotlin/tree/BinaryTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@
1919
import org.junit.jupiter.params.ParameterizedTest;
2020
import org.junit.jupiter.params.provider.ValueSource;
2121
import org.openrewrite.Issue;
22+
import org.openrewrite.ParseExceptionResult;
23+
import org.openrewrite.SourceFile;
24+
import org.openrewrite.kotlin.KotlinParser;
2225
import org.openrewrite.test.RewriteTest;
26+
import org.openrewrite.tree.ParseError;
2327

28+
import java.util.Optional;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
2431
import static org.openrewrite.kotlin.Assertions.kotlin;
2532

2633
@SuppressWarnings({"KotlinConstantConditions", "ControlFlowWithEmptyBody"})
@@ -363,4 +370,29 @@ fun method ( ) {
363370
)
364371
);
365372
}
373+
374+
@Issue("https://github.com/moderneinc/customer-requests/issues/1694")
375+
@Test
376+
void deeplyNestedStringConcatenation() {
377+
// Test parsing deeply nested string concatenations (many + operations)
378+
// With very deep nesting, the Kotlin compiler's FIR builder causes a StackOverflowError
379+
// which should be gracefully caught and converted to a ParseError
380+
StringBuilder sb = new StringBuilder();
381+
sb.append("val s = ");
382+
for (int i = 0; i < 2000; i++) {
383+
sb.append("\"line").append(i).append("\\n\" + ");
384+
}
385+
sb.append("\"end\"");
386+
387+
Optional<SourceFile> sf = KotlinParser.builder().build()
388+
.parse(sb.toString())
389+
.findFirst();
390+
assertThat(sf).isPresent();
391+
assertThat(sf.get()).isInstanceOf(ParseError.class);
392+
ParseError parseError = (ParseError) sf.get();
393+
ParseExceptionResult ex = parseError.getMarkers()
394+
.findFirst(ParseExceptionResult.class)
395+
.orElseThrow();
396+
assertThat(ex.getExceptionType()).isEqualTo("StackOverflowError");
397+
}
366398
}

rewrite-python/src/main/java/org/openrewrite/python/PythonParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ public Stream<SourceFile> parseInputs(Iterable<Input> sources, @Nullable Path re
7474
try {
7575
validator.visit(source, 0);
7676
return source;
77-
} catch (Exception e) {
77+
} catch (Throwable t) {
7878
Optional<Input> input = smallFiles.stream()
7979
.filter(i -> i.getRelativePath(relativeTo).equals(source.getSourcePath()))
8080
.findFirst();
81-
return ParseError.build(this, input.orElseThrow(NoSuchElementException::new), relativeTo, ctx, e);
81+
return ParseError.build(this, input.orElseThrow(NoSuchElementException::new), relativeTo, ctx, t);
8282
}
8383
});
8484
}

0 commit comments

Comments
 (0)