Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? "<constructor>" : node.getName(),
null, finalParamNames, null, null, null, null, null),
method -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<JavaType.Method> ref = new AtomicReference<>();
new GroovyIsoVisitor<Integer>() {
@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);
})
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, "<constructor>",
null, Flag.mapBytecodeAccessFlagsToBitMap(method.getModifiers()), null, "<constructor>",
null, finalParamNames, null, null, null, null, null),
mappedMethod -> {
List<JavaType> thrownExceptions = null;
Expand Down Expand Up @@ -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<JavaType> thrownExceptions = null;
Expand Down
34 changes: 34 additions & 0 deletions rewrite-java/src/main/java/org/openrewrite/java/tree/Flag.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ public static Set<Flag> 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 <em>method or constructor</em> — 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}.
* <p>
* 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.
* <p>
* 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<J.MethodInvocation>>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, List<J.MethodInvocation> found) {
found.add(method);
return super.visitMethodInvocation(method, found);
}
}.reduce(cu, new java.util.ArrayList<J.MethodInvocation>()).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
Expand Down
40 changes: 40 additions & 0 deletions rewrite-java/src/test/java/org/openrewrite/java/tree/FlagTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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);
}
}
Loading