Add source snippet to exception stack trace for easier debugging
What problem are you trying to solve?
When a recipe throws an exception, only the file name of the source file is shown. Quite often that makes guessing the affected source snippet basically impossible, at least that's my experience from previous bug reports. Therefore adding either a short source snippet or the position in the file (line and row, or offset) would really help debugging recipe exceptions.
Describe the solution you'd like
In a prototype, I just added cursor.toString() to the log output that prints the exception stack trace. While that produced far too much output, it helped me identify the relevant source code in many cases, as this produces a kind of "nested snippets" with the uppermost being the relevant piece.
You surely have better ideas how to do that.
Are you interested in contributing this feature to OpenRewrite?
You probably can do that better than me.
This issue is stale because it has not had any activity for 60 days. Remove question label or comment or this will be closed in two weeks. Issues may be reopened when there is renewed interest.
Thanks for raising this issue @Bananeweizen !
@timtebeek Just to clarify - is this about recipe execution errors (not parsing errors)?
If so, I think we could add a snippet field to SourcesFileErrors.Row and implement the extraction logic in RecipeRunCycle.handleError(), following the same pattern as ParseFailures.
Would this address what you're looking for?
hi! Yes this appears to be about unexpected exceptions during recipe execution; linking the code sample you've referenced: https://github.com/openrewrite/rewrite/blob/8ce5308a9557dd6ca91253a5c8fb7e6c34a0ef5a/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java#L289-L307
How did you plan to access the relevant snippet only for the cursor position?
Thanks for the response @timtebeek! Here's my approach
Extract line:column from Range marker
I'll use the existing Range marker to extract position information from the cursor where the error occurred:
private @Nullable String extractLocation(RecipeRunException rre) {
Cursor cursor = rre.getCursor();
if (cursor == null) return null;
Object value = cursor.getValue();
if (!(value instanceof Tree)) return null;
// Extract from Range marker
Optional<Range> range = ((Tree) value).getMarkers().findFirst(Range.class);
if (range.isPresent()) {
Range.Position start = range.get().getStart();
return String.format(":%d:%d", start.getLine(), start.getColumn());
}
return null;
}
Then append this to sourcePath in SourcesFileErrors.Row:
UserService.java:245:12 (instead of just UserService.java)
Benefits:
- Minimal output (avoids "too much output")
- IDE-clickable format
- Standard Java stack trace style
Questions:
Is Range marker always present on Tree nodes during recipe execution?
Thanks!
Thanks! That's my first time seeing Range. From what I can find it's only available for Java files, which would leave quite a bit of code not covered (Yaml, Xml, Properties, ....). Wondering if there's a way to include more details there too, but with my schedule it could be a week+ before I find time to dive in myself.
Wondering if there's a way to include more details there too, but with my schedule it could be a week+ before I find time to dive in myself.
Totally understand @timtebeek! I took a quick look at a few things to help move this forward.
I checked the codebase and found that all file types (.Java, XML, YAML, Properties) use the Markers interface through the Tree interface.
Proposed solution:
private @Nullable String extractLocation(RecipeRunException rre) {
Cursor cursor = rre.getCursor();
if (cursor == null) return null;
Object value = cursor.getValue();
if (!(value instanceof Tree)) return null;
Tree tree = (Tree) value;
// Try Range marker first (works if present)
Optional<Range> range = tree.getMarkers().findFirst(Range.class);
if (range.isPresent()) {
Range.Position start = range.get().getStart();
return String.format(":%d:%d", start.getLine(), start.getColumn());
}
// Fallback: short snippet if no Range
String snippet = tree.printTrimmed(cursor.getParentTreeCursor());
if (snippet != null && snippet.length() > 50) {
return " [" + snippet.substring(0, 50) + "...]";
}
return null;
}
Result:
- Java files:
UserService.java:245:12(if Range exists) - XML/YAML/Properties: Either
:line:col(if Range exists) OR[<dependency>...](snippet fallback)
I think this covers all file types with a single unified method. The fallback ensures we always provide some location context, even if Range markers aren't universally added by all parsers.
Does this approach (Range first, snippet fallback) sound reasonable?