Skip to content

Commit 4157f40

Browse files
Fix AnnotationTemplateGenerator to handle nested annotations correctly (#5713)
* Fix `AnnotationTemplateGenerator` to handle nested annotations correctly The `AnnotationTemplateGenerator` was failing with an `IndexOutOfBoundsException` when processing nested annotations (e.g., repeatable annotations within annotation arrays). This occurred because the template generator couldn't find a "typical annotation target" for annotations nested inside other annotations' array values. The fix adds proper handling for nested annotations by: 1. Walking up the parent hierarchy to find the ultimate annotation target (method, variable, or class) 2. Adding a fallback case that generates `@interface $Placeholder {}` when no typical target can be found 3. Updating both the `template()` and `cacheKey()` methods to handle nested annotation cases consistently This ensures that `JavaTemplate` can successfully process and transform nested annotations without throwing exceptions. Added a test case that demonstrates the fix by transforming nested repeatable annotations' attributes using `JavaTemplate`. Fixes: - #5712 * Remove unnecessary code * Remove need for `TypeValidation.none()` * Trim empty lines * Trim empty lines * Remove unnecessary branch --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent a998126 commit 4157f40

4 files changed

Lines changed: 83 additions & 5 deletions

File tree

.claude/settings.local.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
"Bash(grep:*)",
1010
"Bash(mv:*)",
1111
"Bash(npm test:*)",
12+
"Bash(rg:*)",
1213
"Bash(rm:*)",
1314
"Bash(timeout:*)",
1415
"WebFetch(domain:github.com)",
1516
"mcp__github__get_issue",
17+
"mcp__github__get_issue_comments",
18+
"mcp__github__get_pull_request",
19+
"mcp__github__get_pull_request_comments",
1620
"mcp__idea__find_files_by_name_substring",
1721
"mcp__idea__get_file_text_by_path",
1822
"mcp__idea__get_open_in_editor_file_path",

rewrite-java-test/src/test/java/org/openrewrite/java/JavaTemplateAnnotationTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.openrewrite.java;
1717

18+
import org.intellij.lang.annotations.Language;
1819
import org.junit.jupiter.api.Test;
1920
import org.openrewrite.DocumentExample;
2021
import org.openrewrite.ExecutionContext;
@@ -158,4 +159,75 @@ public record Person(
158159
)
159160
);
160161
}
162+
163+
@Issue("https://github.com/openrewrite/rewrite/issues/5712")
164+
@Test
165+
void replaceArgumentsInNestedAnnotation() {
166+
@Language("java")
167+
String annotations = """
168+
package foo;
169+
170+
import java.lang.annotation.*;
171+
172+
@Repeatable(NestedAnnotations.class)
173+
public @interface NestedAnnotation {
174+
String a() default "";
175+
String b() default "";
176+
}
177+
178+
public @interface NestedAnnotations {
179+
NestedAnnotation[] value();
180+
}
181+
""";
182+
rewriteRun(
183+
spec -> spec
184+
.parser(JavaParser.fromJavaVersion().dependsOn(annotations))
185+
.recipe(toRecipe(() -> new JavaIsoVisitor<>() {
186+
@Override
187+
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
188+
if (annotation.getSimpleName().equals("NestedAnnotation") &&
189+
!annotation.getArguments().isEmpty()) {
190+
// Check if this annotation still has the 'a' attribute that needs to be replaced
191+
J.Assignment arg = (J.Assignment) annotation.getArguments().get(0);
192+
if (arg.getVariable() instanceof J.Identifier &&
193+
((J.Identifier) arg.getVariable()).getSimpleName().equals("a")) {
194+
// Only apply the template if we haven't already transformed this annotation
195+
J.Literal value = (J.Literal) arg.getAssignment();
196+
197+
// Replace 'a' with 'b' in the annotation
198+
return JavaTemplate.builder("@NestedAnnotation(b = #{any(java.lang.String)})")
199+
.javaParser(JavaParser.fromJavaVersion().dependsOn(annotations))
200+
.imports("foo.*")
201+
.build()
202+
.apply(getCursor(), annotation.getCoordinates().replace(), value);
203+
}
204+
}
205+
return super.visitAnnotation(annotation, ctx);
206+
}
207+
})),
208+
java(
209+
"""
210+
import foo.*;
211+
212+
@NestedAnnotations({
213+
@NestedAnnotation(a = "1"),
214+
@NestedAnnotation(a = "2")
215+
})
216+
class Test {
217+
}
218+
""",
219+
"""
220+
import foo.*;
221+
222+
@NestedAnnotations({
223+
@NestedAnnotation(b = "1"),
224+
@NestedAnnotation(b = "2")
225+
})
226+
class Test {
227+
}
228+
"""
229+
)
230+
);
231+
}
232+
161233
}

rewrite-java/src/main/java/org/openrewrite/java/internal/template/AnnotationTemplateGenerator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ public String template(Cursor cursor, String template) {
8989
after.insert(0, " void $method() {}");
9090
} else if (j instanceof J.VariableDeclarations || annotationParent instanceof J.VariableDeclarations) {
9191
after.insert(0, " int $variable;");
92-
} else if (j instanceof J.ClassDeclaration) {
93-
if (cursor.getParentOrThrow().getValue() instanceof JavaSourceFile) {
92+
} else if (j instanceof J.ClassDeclaration || annotationParent instanceof J.ClassDeclaration) {
93+
// Check if this is a top-level class or nested class
94+
Cursor classCursor = j instanceof J.ClassDeclaration ? cursor : cursor.getParent(level);
95+
if (classCursor != null && classCursor.getParentOrThrow().getValue() instanceof JavaSourceFile) {
9496
after.insert(0, "class $Clazz {}");
9597
} else {
9698
after.insert(0, "static class $Clazz {}");

rewrite-java/src/main/java/org/openrewrite/java/internal/template/JavaTemplateJavaExtension.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public TreeVisitor<? extends J, Integer> getMixin() {
5757
private boolean substituted;
5858

5959
@Override
60-
public J visitAnnotation(J.Annotation annotation, Integer integer) {
60+
public J visitAnnotation(J.Annotation annotation, Integer p) {
6161
if (loc == ANNOTATION_PREFIX && mode == JavaCoordinates.Mode.REPLACEMENT &&
6262
isScope(annotation)) {
6363
List<J.Annotation> gen = unsubstitute(templateParser.parseAnnotations(getCursor(), substitutedTemplate));
@@ -66,14 +66,14 @@ public J visitAnnotation(J.Annotation annotation, Integer integer) {
6666
substitutedTemplate +
6767
"\nUse JavaTemplate.Builder.doBeforeParseTemplate() to see what stub is being generated and include it in any bug report.");
6868
}
69-
return gen.get(0).withPrefix(annotation.getPrefix());
69+
return autoFormat(gen.get(0).withPrefix(annotation.getPrefix()), p, getCursor().getParentOrThrow());
7070
} else if (loc == ANNOTATION_ARGUMENTS && mode == JavaCoordinates.Mode.REPLACEMENT &&
7171
isScope(annotation)) {
7272
List<J.Annotation> gen = unsubstitute(templateParser.parseAnnotations(getCursor(), "@Example(" + substitutedTemplate + ")"));
7373
return annotation.withArguments(gen.get(0).getArguments());
7474
}
7575

76-
return super.visitAnnotation(annotation, integer);
76+
return super.visitAnnotation(annotation, p);
7777
}
7878

7979
@Override

0 commit comments

Comments
 (0)