diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyTypeMapping.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyTypeMapping.java index 4c7076aabe2..9b7d301b9b4 100644 --- a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyTypeMapping.java +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyTypeMapping.java @@ -208,7 +208,7 @@ private JavaType genericType(GenericsType g, String signature) { String[] finalParamNames = paramNames; return typeFactory.methodFor(signature, () -> new JavaType.Method( - null, node.getModifiers(), null, + null, Flag.mapBytecodeAccessFlagsToBitMap(node.getModifiers()), null, node instanceof ConstructorNode ? "" : node.getName(), null, finalParamNames, null, null, null, null, null), method -> { diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/GroovyVarargsTypeMappingTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/GroovyVarargsTypeMappingTest.java new file mode 100644 index 00000000000..5c038429731 --- /dev/null +++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/GroovyVarargsTypeMappingTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.groovy; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.tree.Flag; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.test.RewriteTest; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.groovy.Assertions.groovy; + +class GroovyVarargsTypeMappingTest implements RewriteTest { + + @Test + void varargsMethodHasVarargsFlag() { + // Groovy resolves library methods from byte code, where varargs is encoded as + // ACC_VARARGS (0x80) in MethodNode.getModifiers(). That bit collides with + // Flag.Transient, so without remapping the call would be flagged Transient and + // lose Varargs -- the upstream cause of UseDiamondOperator's AIOOBE. + rewriteRun( + groovy( + """ + java.util.Arrays.asList("a", "b", "c") + """, + spec -> spec.afterRecipe(cu -> { + AtomicReference ref = new AtomicReference<>(); + new GroovyIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Integer p) { + if (method.getMethodType() != null && "asList".equals(method.getMethodType().getName())) { + ref.set(method.getMethodType()); + } + return super.visitMethodInvocation(method, p); + } + }.visit(cu, 0); + + assertThat(ref.get()).as("asList method type resolved").isNotNull(); + assertThat(ref.get().getFlags()) + .contains(Flag.Varargs) + .doesNotContain(Flag.Transient); + }) + ) + ); + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaReflectionTypeMapping.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaReflectionTypeMapping.java index a6097af57a9..0bb40dfb630 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaReflectionTypeMapping.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaReflectionTypeMapping.java @@ -17,6 +17,7 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.java.JavaTypeMapping; +import org.openrewrite.java.tree.Flag; import org.openrewrite.java.tree.JavaType; import java.lang.annotation.Annotation; @@ -305,7 +306,7 @@ private JavaType.Method method(Constructor method, JavaType.FullyQualified de String[] finalParamNames = paramNames; return typeFactory.methodFor(signature, () -> new JavaType.Method( - null, method.getModifiers(), null, "", + null, Flag.mapBytecodeAccessFlagsToBitMap(method.getModifiers()), null, "", null, finalParamNames, null, null, null, null, null), mappedMethod -> { List thrownExceptions = null; @@ -413,7 +414,7 @@ private JavaType.Method method(Method method, JavaType.FullyQualified declaringT String[] finalFormalTypeNames = declaredFormalTypeNames == null ? null : declaredFormalTypeNames.toArray(new String[0]); return typeFactory.methodFor(signature, () -> new JavaType.Method( - null, method.getModifiers(), null, method.getName(), + null, Flag.mapBytecodeAccessFlagsToBitMap(method.getModifiers()), null, method.getName(), null, finalParamNames, null, null, null, finalDefaultValues, finalFormalTypeNames), mappedMethod -> { List thrownExceptions = null; diff --git a/rewrite-java/src/main/java/org/openrewrite/java/tree/Flag.java b/rewrite-java/src/main/java/org/openrewrite/java/tree/Flag.java index 69ac5dc9f04..d91349ff148 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/tree/Flag.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/tree/Flag.java @@ -91,6 +91,40 @@ public static Set bitMapToFlags(long flagsBitMap) { return flags; } + /** + * The JVM/bytecode {@code ACC_VARARGS} access flag, which is also the bit + * {@code java.lang.reflect.Member#getModifiers()} and ASM report for varargs methods. + * It occupies bit {@code 0x0080} — the very same bit this enum assigns to {@link #Transient}. + * Varargs, by contrast, is modeled by {@link #Varargs} ({@code 1L << 34}, matching javac's + * {@code com.sun.tools.javac.code.Flags.VARARGS}). + */ + private static final long ACC_VARARGS = 0x0080; + + /** + * Translate a bytecode-level access-flags bitmap of a method or constructor — as + * produced by ASM or {@code java.lang.reflect.Member#getModifiers()}, where + * {@code ACC_VARARGS == 0x0080} — into the canonical bitmap consumed by + * {@link #bitMapToFlags(long)} and stored on {@link org.openrewrite.java.tree.JavaType.Method}. + *

+ * The {@code ACC_VARARGS} bit collides with {@link #Transient}. Because {@code transient} is + * meaningless on an executable, the bit is rewritten to {@link #Varargs} so that varargs methods + * carry the correct flag rather than a spurious {@code Transient}. Flags that originate from + * javac already use {@link #Varargs}'s bit and never set {@code 0x0080} on an executable, so they + * pass through unchanged. + *

+ * Do not apply this to field access flags: for fields {@code 0x0080} legitimately means + * {@code transient}. + * + * @param accessFlags bytecode access flags for a method or constructor + * @return the access flags with {@code ACC_VARARGS} remapped to {@link #Varargs} + */ + public static long mapBytecodeAccessFlagsToBitMap(long accessFlags) { + if ((accessFlags & ACC_VARARGS) != 0) { + return (accessFlags & ~ACC_VARARGS) | Varargs.bitMask; + } + return accessFlags; + } + /** * Converts a set of flag enumerations into the Java Language Specification's access_flags bitmap * diff --git a/rewrite-java/src/test/java/org/openrewrite/java/internal/JavaReflectionTypeMappingTest.java b/rewrite-java/src/test/java/org/openrewrite/java/internal/JavaReflectionTypeMappingTest.java index b68ddaec4fe..23b714a2a73 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/internal/JavaReflectionTypeMappingTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/internal/JavaReflectionTypeMappingTest.java @@ -19,9 +19,12 @@ import org.junit.jupiter.api.Test; import org.openrewrite.java.JavaTypeGoat; import org.openrewrite.java.JavaTypeMappingTest; +import org.openrewrite.java.tree.Flag; import org.openrewrite.java.tree.JavaType; import org.openrewrite.java.tree.TypeUtils; +import java.util.Arrays; + import static org.assertj.core.api.Assertions.assertThat; @SuppressWarnings("ConstantConditions") @@ -93,4 +96,15 @@ void syntheticEnumConstructorParametersAreExcluded() { assertThat(constructor.getParameterNames()).hasSameSizeAs(constructor.getParameterTypes()); } + @Test + void varargsMethodHasVarargsFlag() throws Exception { + // Reflection exposes varargs as ACC_VARARGS (0x80) in getModifiers(), which collides + // with Flag.Transient. The mapping must translate it to Flag.Varargs rather than + // leaving a spurious Transient. + JavaType.Method asList = typeMapping.method(Arrays.class.getMethod("asList", Object[].class)); + assertThat(asList.getFlags()) + .contains(Flag.Varargs) + .doesNotContain(Flag.Transient); + } + } diff --git a/rewrite-java/src/test/java/org/openrewrite/java/internal/parser/TypeTableTest.java b/rewrite-java/src/test/java/org/openrewrite/java/internal/parser/TypeTableTest.java index fe46b2c438c..2ac6848637f 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/internal/parser/TypeTableTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/internal/parser/TypeTableTest.java @@ -526,6 +526,67 @@ void writeReadWithAnnotations() throws Exception { } } + @Test + void varargsFlagPreservedThroughTypeTableRoundtrip() throws Exception { + //language=java + String libraryClass = """ + package test.library; + + public class VarargsLibrary { + public static String join(String separator, Object... args) { + return null; + } + } + """; + + Path[] classFiles = compileToClassFiles( + libraryClass, "test.library.VarargsLibrary" + ); + Path testJar = createJarFromClasses("varargs-library.jar", classFiles); + + // Write through TypeTable + try (TypeTable.Writer writer = TypeTable.newWriter(Files.newOutputStream(tsv))) { + writer.jar("test.group", "varargs-library", "1.0").write(testJar); + } + + // Load back via TypeTable + var table = new TypeTable(ctx, tsv.toUri().toURL(), List.of("varargs-library")); + Path classesDir = table.load("varargs-library"); + assertThat(classesDir).isNotNull(); + + rewriteRun( + spec -> spec.parser((Parser.Builder) JavaParser.fromJavaVersion() + .classpath(List.of(classesDir)).logCompilationWarningsAndErrors(true)), + java( + """ + import test.library.VarargsLibrary; + + class TestClient { + void use() { + VarargsLibrary.join(",", "a", "b"); + } + } + """, + spec -> spec.afterRecipe(cu -> { + J.MethodInvocation invocation = new org.openrewrite.java.JavaIsoVisitor>() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, List found) { + found.add(method); + return super.visitMethodInvocation(method, found); + } + }.reduce(cu, new java.util.ArrayList()).getFirst(); + + JavaType.Method methodType = invocation.getMethodType(); + assertThat(methodType).isNotNull(); + assertThat(methodType.getName()).isEqualTo("join"); + assertThat(methodType.getFlags()) + .as("Varargs flag must survive the TypeTable round-trip") + .contains(org.openrewrite.java.tree.Flag.Varargs); + }) + ) + ); + } + @Test void annotationAttributeValuesPreservedThroughTypeTableRoundtrip() throws Exception { // Create annotation with various default values to test escaping and preservation diff --git a/rewrite-java/src/test/java/org/openrewrite/java/tree/FlagTest.java b/rewrite-java/src/test/java/org/openrewrite/java/tree/FlagTest.java new file mode 100644 index 00000000000..a94be2776cb --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/tree/FlagTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.tree; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FlagTest { + + // ACC_PUBLIC | ACC_STATIC | ACC_VARARGS, e.g. java.util.Arrays.asList + private static final long PUBLIC_STATIC_VARARGS = 0x0001 | 0x0008 | 0x0080; + + @Test + void mapsAccVarargsToVarargsFlag() { + long bitMap = Flag.mapBytecodeAccessFlagsToBitMap(PUBLIC_STATIC_VARARGS); + assertThat(Flag.bitMapToFlags(bitMap)) + .contains(Flag.Public, Flag.Static, Flag.Varargs) + .doesNotContain(Flag.Transient); + } + + @Test + void leavesNonVarargsFlagsUntouched() { + long publicStatic = 0x0001 | 0x0008; + assertThat(Flag.mapBytecodeAccessFlagsToBitMap(publicStatic)).isEqualTo(publicStatic); + } +}