diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 8356dfd..910bb5e 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -20,3 +20,7 @@ dependencies { fun Provider.withVersion(version: String): Provider { return map { "${it.module.group}:${it.module.name}:$version" } } + +kotlin { + jvmToolchain(21) +} diff --git a/build-logic/src/main/kotlin/config-java25.gradle.kts b/build-logic/src/main/kotlin/config-java25.gradle.kts new file mode 100644 index 0000000..69b12c9 --- /dev/null +++ b/build-logic/src/main/kotlin/config-java25.gradle.kts @@ -0,0 +1,79 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.incendo.cloudbuildlogic.jmp + +plugins { + id("net.kyori.indra") + id("net.kyori.indra.publishing") + id("net.kyori.indra.checkstyle") + id("org.incendo.cloud-build-logic.javadoc-links") +} + +val libs = the() + +indra { + javaVersions { + target(25) + strictVersions(true) + } + + publishSnapshotsTo("paperSnapshots", "https://artifactory.papermc.io/artifactory/snapshots/") + publishReleasesTo("paperReleases", "https://artifactory.papermc.io/artifactory/releases/") + signWithKeyFromProperties("signingKey", "signingPassword") + + apache2License() + + github("PaperMC", "asm-utils") { + ci(true) + } + + configurePublications { + pom { + developers { + jmp() + developer { + id = "Machine-Maker" + name = "Jake Potrebic" + url = "https://github.com/Machine-Maker" + } + developer { + id = "kennytv" + name = "Nassim Jahnke" + url = "https://github.com/kennytv" + } + } + } + } +} + +repositories { + mavenCentral() +} + +val mockitoAgent = configurations.create("mockitoAgent") + +dependencies { + compileOnlyApi(libs.jspecify) + testCompileOnly(libs.jspecify) + compileOnly(libs.jetbrainsAnnotations) + testCompileOnly(libs.jetbrainsAnnotations) + + mockitoAgent(libs.mockito.core) { isTransitive = false } + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.junit) + testImplementation(libs.assertj) + testImplementation(libs.jupiterApi) + testImplementation(libs.jupiterParams) + testRuntimeOnly(libs.jupiterEngine) + testRuntimeOnly(libs.platformLauncher) +} + +tasks { + test { + useJUnitPlatform() + jvmArgs("-javaagent:${mockitoAgent.asPath}") + } +} + +javadocLinks { + override(libs.jspecify, "https://jspecify.dev/docs/api/") +} diff --git a/classfile-utils/build.gradle.kts b/classfile-utils/build.gradle.kts new file mode 100644 index 0000000..6263ba1 --- /dev/null +++ b/classfile-utils/build.gradle.kts @@ -0,0 +1,88 @@ +import org.gradle.kotlin.dsl.register +import java.nio.file.Files +import kotlin.io.path.copyTo +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.isDirectory +import kotlin.use + +plugins { + id("config-java25") +} + +val mainForNewTargets = sourceSets.create("mainForNewTargets") + +val testDataSet = sourceSets.create("testData") +val testDataNewTargets = sourceSets.create("testDataNewTargets") + +val filtered = tasks.register("filteredTestClasspath") { + outputDir.set(layout.buildDirectory.dir("filteredTestClasspath")) + old.from(testDataSet.output) + new.from(testDataNewTargets.output) +} + +dependencies { + api(mainForNewTargets.output) + testRuntimeOnly(files(filtered.flatMap { it.outputDir })) // only have access to old targets at runtime, don't use them in actual tests + testImplementation(testDataNewTargets.output) + + testDataSet.compileOnlyConfigurationName(libs.jspecify) + testDataNewTargets.compileOnlyConfigurationName(libs.jspecify) + testDataNewTargets.implementationConfigurationName(mainForNewTargets.output) +} + +abstract class FilterTestClasspath : DefaultTask() { + @get:InputFiles + abstract val old: ConfigurableFileCollection + + @get:InputFiles + abstract val new: ConfigurableFileCollection + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:Inject + abstract val fsOps: FileSystemOperations + + @TaskAction + fun run() { + if (!outputDir.get().asFile.toPath().exists()) { + outputDir.get().asFile.mkdirs() + } else { + fsOps.delete { + delete(outputDir.get()) + } + outputDir.get().asFile.mkdirs() + } + + val newExisting = mutableListOf() + for (file in new.files) { + if (file.exists()) { + Files.walk(file.toPath()).use { s -> + s.forEach { + if (it.isDirectory()) { + return@forEach + } + newExisting += file.toPath().relativize(it).invariantSeparatorsPathString + } + } + } + } + for (file in old.files) { + if (file.exists()) { + Files.walk(file.toPath()).use { s -> + s.forEach { + if (it.isDirectory()) { + return@forEach + } + val rel = file.toPath().relativize(it).invariantSeparatorsPathString + if (rel !in newExisting) { + it.copyTo(outputDir.get().asFile.toPath().resolve(rel).also { f -> f.parent.createDirectories() }) + } + } + } + } + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java new file mode 100644 index 0000000..51fcc25 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java @@ -0,0 +1,105 @@ +package io.papermc.classfile; + +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.Opcode; +import java.lang.classfile.TypeKind; +import java.lang.constant.ClassDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.invoke.LambdaMetafactory; +import java.util.Set; +import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + +public final class ClassFiles { + + public static final int BOOTSTRAP_HANDLE_IDX = 1; + public static final int DYNAMIC_TYPE_IDX = 2; + public static final String CONSTRUCTOR_METHOD_NAME = ""; + public static final ClassDesc LAMBDA_METAFACTORY = desc(LambdaMetafactory.class); + public static final String GENERATED_PREFIX = "paperClassfileGenerated$"; + private static final String DEFAULT_CTOR_METHOD_PREFIX = "create"; + + private ClassFiles() { + } + + public static String toInternalName(final ClassDesc clazz) { + if (!clazz.isClassOrInterface()) { + throw new IllegalArgumentException("Not a class or interface: " + clazz); + } + return clazz.descriptorString().substring(1, clazz.descriptorString().length() - 1); + } + + public static ClassDesc desc(final Class clazz) { + return clazz.describeConstable().orElseThrow(); + } + + public static MethodTypeDesc adjustForStatic(final Opcode opcode, final ClassDesc owner, final MethodTypeDesc descriptor) { + return switch (opcode) { + // for INVOKEVIRTUAL, INVOKEINTERFACE methods, we have to add the receiver as the first param for the static replacement + case INVOKEVIRTUAL, INVOKEINTERFACE -> descriptor.insertParameterTypes(0, owner); + // for INVOKESPECIAL, we have to add a return type; constructors have a void return type + case INVOKESPECIAL -> descriptor.changeReturnType(owner); + case INVOKESTATIC -> descriptor; + default -> throw new IllegalArgumentException("Unexpected opcode: " + opcode); + }; + } + + public static MethodTypeDesc adjustForStatic(final DirectMethodHandleDesc.Kind kind, final ClassDesc owner, final MethodTypeDesc descriptor) { + return switch (kind) { + // for VIRTUAL, INTERFACE_VIRTUAL methods, we have to add the receiver as the first param for the static replacement + case VIRTUAL, INTERFACE_VIRTUAL -> descriptor.insertParameterTypes(0, owner); + // for CONSTRUCTOR, we have to add a return type; constructors have a void return type + case CONSTRUCTOR -> descriptor.changeReturnType(owner); + case STATIC, INTERFACE_STATIC -> descriptor; + default -> throw new IllegalArgumentException("Unexpected kind: " + kind); + }; + } + + public static MethodTypeDesc replaceParameters(MethodTypeDesc descriptor, final Predicate oldParam, final ClassDesc newParam) { + for (int i = 0; i < descriptor.parameterCount(); i++) { + if (oldParam.test(descriptor.parameterType(i))) { + descriptor = descriptor.changeParameterType(i, newParam); + } + } + return descriptor; + } + + public static String constructorMethodName(final ClassDesc owner) { + // strip preceding "L" and trailing ";"" + final String ownerName = owner.descriptorString().substring(1, owner.descriptorString().length() - 1); + return DEFAULT_CTOR_METHOD_PREFIX + ownerName.substring(ownerName.lastIndexOf('/') + 1); + } + + public static void emitInvoke(final CodeBuilder cb, final Opcode opcode, final MethodTransformContext.MethodInfo info, final MethodTypeDesc callDesc, final boolean includeSpecial) { + switch (opcode) { + case INVOKEVIRTUAL -> cb.invokevirtual(info.owner(), info.name(), callDesc); + case INVOKEINTERFACE -> cb.invokeinterface(info.owner(), info.name(), callDesc); + case INVOKESTATIC -> cb.invokestatic(info.owner(), info.name(), callDesc, info.isInterface()); + case INVOKESPECIAL -> { + if (!includeSpecial) { + throw new IllegalArgumentException("INVOKESPECIAL is not supported here"); + } + cb.invokespecial(info.owner(), info.name(), callDesc, false); + } + default -> throw new IllegalArgumentException(opcode.toString()); + } + } + + public static void emitInvoke(final CodeBuilder cb, final DirectMethodHandleDesc.Kind kind, final MethodTransformContext.MethodInfo info, final MethodTypeDesc callDesc, final boolean includeSpecial) { + switch (kind) { + case VIRTUAL -> cb.invokevirtual(info.owner(), info.name(), callDesc); + case INTERFACE_VIRTUAL -> cb.invokeinterface(info.owner(), info.name(), callDesc); + case STATIC -> cb.invokestatic(info.owner(), info.name(), callDesc, false); + case INTERFACE_STATIC -> cb.invokestatic(info.owner(), info.name(), callDesc, true); + case CONSTRUCTOR -> { + if (!includeSpecial) { + throw new IllegalArgumentException("INVOKESPECIAL is not supported here"); + } + cb.invokespecial(info.owner(), CONSTRUCTOR_METHOD_NAME, callDesc, false); + } + default -> throw new IllegalArgumentException(kind.toString()); + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/MethodType.java b/classfile-utils/src/main/java/io/papermc/classfile/MethodType.java new file mode 100644 index 0000000..ed0a5ef --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/MethodType.java @@ -0,0 +1,6 @@ +package io.papermc.classfile; + +public enum MethodType { + + +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java new file mode 100644 index 0000000..54d25e4 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java @@ -0,0 +1,31 @@ +package io.papermc.classfile; + +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.MethodRewriteIndex; +import io.papermc.classfile.method.transform.BridgeMethodRegistry; +import io.papermc.classfile.transform.TransformContext; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.util.List; + +public class RewriteProcessor { + + private static final ClassFile CLASS_FILE = ClassFile.of(); + + private final MethodRewriteIndex methodIndex; + + public RewriteProcessor(final List methodRewrites) { + this.methodIndex = new MethodRewriteIndex(methodRewrites); + } + + public byte[] rewrite(final byte[] input) { + final ClassModel inputModel = CLASS_FILE.parse(input); + final BridgeMethodRegistry bridges = new BridgeMethodRegistry(); + final TransformContext context = TransformContext.create(inputModel.thisClass().asSymbol(), bridges); + final ClassTransform transform = ClassTransform.transformingMethods(MethodRewrite.createTransform(this.methodIndex, context)) + .andThen(ClassTransform.endHandler(bridges::emitAll)); + return CLASS_FILE.transformClass(inputModel, transform); + } + +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/generation/ParameterGeneration.java b/classfile-utils/src/main/java/io/papermc/classfile/generation/ParameterGeneration.java new file mode 100644 index 0000000..ea3d1af --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/generation/ParameterGeneration.java @@ -0,0 +1,69 @@ +package io.papermc.classfile.generation; + +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.TypeKind; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.function.Consumer; + +@FunctionalInterface +public interface ParameterGeneration { + + static ParameterGeneration standard() { + return Standard.INSTANCE; + } + + static ParameterGeneration standard(final Consumer prefix) { + return (descriptor, builder) -> { + prefix.accept(builder); + Standard.INSTANCE.generateParameters(descriptor, builder); + }; + } + + static ParameterGeneration mutating(final Mutating mutator) { + return mutating($ -> {}, mutator); + } + + static ParameterGeneration mutating(final Consumer prefix, final Mutating mutator) { + return (descriptor, builder) -> { + prefix.accept(builder); + mutator.generateParameters(descriptor, builder); + }; + } + + void generateParameters(MethodTypeDesc descriptor, CodeBuilder builder); + + record Standard() implements ParameterGeneration { + + private static final Standard INSTANCE = new Standard(); + + @Override + public void generateParameters(final MethodTypeDesc descriptor, final CodeBuilder builder) { + // Load all parameters (using correct slots for wide types) + int slot = 0; + for (final ClassDesc paramType : descriptor.parameterList()) { + final TypeKind typeKind = TypeKind.from(paramType); + builder.loadLocal(typeKind, slot); + slot += typeKind.slotSize(); + } + } + } + + @FunctionalInterface + interface Mutating extends ParameterGeneration { + + @Override + default void generateParameters(final MethodTypeDesc descriptor, final CodeBuilder builder) { + // Load all parameters (using correct slots for wide types) + int slot = 0; + for (final ClassDesc paramType : descriptor.parameterList()) { + final TypeKind typeKind = TypeKind.from(paramType); + builder.loadLocal(typeKind, slot); + this.mutate(paramType, builder); + slot += typeKind.slotSize(); + } + } + + void mutate(ClassDesc paramType, CodeBuilder builder); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodDescriptorPredicate.java b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodDescriptorPredicate.java new file mode 100644 index 0000000..00146a9 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodDescriptorPredicate.java @@ -0,0 +1,34 @@ +package io.papermc.classfile.method; + +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.function.Predicate; + +public sealed interface MethodDescriptorPredicate extends Predicate { + + ClassDesc targetType(); + + static MethodDescriptorPredicate hasReturn(final ClassDesc returnType) { + return new HasReturn(returnType); + } + + static MethodDescriptorPredicate hasParameter(final ClassDesc parameterType) { + return new HasParameter(parameterType); + } + + record HasReturn(ClassDesc targetType) implements MethodDescriptorPredicate { + + @Override + public boolean test(final MethodTypeDesc methodTypeDesc) { + return methodTypeDesc.returnType().equals(this.targetType); + } + } + + record HasParameter(ClassDesc targetType) implements MethodDescriptorPredicate { + + @Override + public boolean test(final MethodTypeDesc methodTypeDesc) { + return methodTypeDesc.parameterList().contains(this.targetType); + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java new file mode 100644 index 0000000..24ac550 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java @@ -0,0 +1,69 @@ +package io.papermc.classfile.method; + +import io.papermc.classfile.ClassFiles; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +public sealed interface MethodNamePredicate extends Predicate { + + static MethodNamePredicate constructor() { + final class Holder { + static final MethodNamePredicate INSTANCE = new Constructor(); + } + return Holder.INSTANCE; + } + + static MethodNamePredicate exact(final String name, final String... otherNames) { + final List names = new ArrayList<>(); + names.add(name); + names.addAll(List.of(otherNames)); + return exact(names); + } + + static MethodNamePredicate exact(final Collection names) { + return new ExactMatch(new ArrayList<>(names)); + } + + static MethodNamePredicate prefix(final String prefix) { + return new PrefixMatch(prefix); + } + + record ExactMatch(List names) implements MethodNamePredicate { + + public ExactMatch { + if (names.stream().anyMatch(s -> s.equals(ClassFiles.CONSTRUCTOR_METHOD_NAME))) { + throw new IllegalArgumentException("Cannot use as a method name, use the dedicated constructor predicate"); + } + names = List.copyOf(names); + } + + @Override + public boolean test(final String s) { + return this.names.stream().anyMatch(s::equals); + } + } + + record Constructor() implements MethodNamePredicate { + + @Override + public boolean test(final String charSequence) { + return ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(charSequence); + } + } + + record PrefixMatch(String prefix) implements MethodNamePredicate { + + public PrefixMatch { + if (ClassFiles.CONSTRUCTOR_METHOD_NAME.startsWith(prefix)) { + throw new IllegalArgumentException("Cannot use as a method name, use the dedicated constructor predicate"); + } + } + + @Override + public boolean test(final String s) { + return s.startsWith(this.prefix); + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java new file mode 100644 index 0000000..5bf104b --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java @@ -0,0 +1,72 @@ +package io.papermc.classfile.method; + +import io.papermc.classfile.method.action.MethodRewriteAction; +import io.papermc.classfile.method.transform.ConstructorAwareCodeTransform; +import io.papermc.classfile.method.transform.MethodTransformContext; +import io.papermc.classfile.method.transform.SimpleMethodBodyTransform; +import io.papermc.classfile.method.transform.TrackingConsumer; +import io.papermc.classfile.transform.TransformContext; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.MethodTransform; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.util.List; + +public record MethodRewrite(ClassDesc owner, MethodNamePredicate methodName, MethodDescriptorPredicate descriptor, MethodRewriteAction action) { + + public MethodRewrite { + action.isValidFor(methodName, descriptor).ifPresent(s -> { + throw new IllegalArgumentException(s); + }); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean doesMatch(final MethodTransformContext.MethodInfo info) { + return this.methodName.test(info.name()) && this.descriptor.test(info.descriptor()); + } + + public boolean transformInvoke(final MethodTransformContext context, final Opcode opcode) { + // owner validated by caller + if (!this.doesMatch(context.methodInfo())) { + return false; + } + try (TrackingConsumer.Instance _ = context.prime()) { + this.action.rewriteInvoke(context, opcode); + } + return true; + } + + public boolean transformInvokeDynamic( + final MethodTransformContext context, + final DirectMethodHandleDesc bootstrapMethod, + final DirectMethodHandleDesc methodHandle, + final List args, + final InvokeDynamicInstruction invokeDynamic + ) { + // owner validated by caller + if (!this.doesMatch(context.methodInfo())) { + return false; + } + final MethodRewriteAction.BootstrapInfo info = new MethodRewriteAction.BootstrapInfo(bootstrapMethod, invokeDynamic.name().stringValue(), invokeDynamic.typeSymbol(), args); + try (TrackingConsumer.Instance _ = context.prime()) { + this.action.rewriteInvokeDynamic(context, methodHandle.kind(), info); + } + return true; + } + + public static MethodTransform createTransform(final MethodRewriteIndex index, final TransformContext context) { + final SimpleMethodBodyTransform basicTransform = new SimpleMethodBodyTransform(index, context); + final boolean constructorRewrites = index.hasConstructorRewrites(); + if (!constructorRewrites) { + return MethodTransform.transformingCode(basicTransform); + } + return MethodTransform.transformingCode(CodeTransform.ofStateful(() -> { + return new ConstructorAwareCodeTransform(index, basicTransform, context); + })); + } + +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewriteIndex.java b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewriteIndex.java new file mode 100644 index 0000000..79d9721 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewriteIndex.java @@ -0,0 +1,106 @@ +package io.papermc.classfile.method; + +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class MethodRewriteIndex { + + private final Map index; + private final Map> ctorIndex; + + public MethodRewriteIndex(final List rewrites) { + final Map mutable = new HashMap<>(); + final Map> ctorIndex = new HashMap<>(); + for (final MethodRewrite rewrite : rewrites) { + if (rewrite.methodName() instanceof MethodNamePredicate.Constructor) { + final List existingRewrites = ctorIndex.computeIfAbsent(rewrite.owner(), $ -> new ArrayList<>()); + existingRewrites.add(rewrite); + // TODO check that we can still add them to the index + // continue; + } + final NameIndex nameIndex = mutable.computeIfAbsent(rewrite.owner(), _ -> new NameIndex()); + final List exactNames = exactNames(rewrite.methodName()); + if (!exactNames.isEmpty()) { + exactNames.forEach(name -> nameIndex.add(rewrite, name)); + } else { + nameIndex.addWildcard(rewrite); + } + } + this.ctorIndex = ctorIndex.entrySet().stream().collect(Collectors.toUnmodifiableMap( + Map.Entry::getKey, + e -> List.copyOf(e.getValue()) + )); + this.index = mutable.entrySet().stream().collect(Collectors.toUnmodifiableMap( + Map.Entry::getKey, + e -> e.getValue().toImmutable() + )); + } + + private static List exactNames(final MethodNamePredicate predicate) { + if (!(predicate instanceof MethodNamePredicate.ExactMatch(final List names))) { + return Collections.emptyList(); + } + return names; + } + + public boolean hasConstructorRewrites() { + return !this.ctorIndex.isEmpty(); + } + + public List constructorCandidates(final ClassDesc owner) { + final List rewrites = this.ctorIndex.get(owner); + if (rewrites == null) { + return Collections.emptyList(); + } + return rewrites; + } + + public List candidates(final ClassDesc owner, final String methodName) { + final NameIndex nameIndex = this.index.get(owner); + if (nameIndex == null) { + return Collections.emptyList(); + } + return nameIndex.candidates(methodName); + } + + private record NameIndex(Map> exact, List wildcards) { + + NameIndex() { + this(new HashMap<>(), new ArrayList<>()); + } + + void add(final MethodRewrite rewrite, final String exactName) { + this.exact.computeIfAbsent(exactName, _ -> new ArrayList<>()).add(rewrite); + } + + void addWildcard(final MethodRewrite rewrite) { + this.wildcards.add(rewrite); + } + + List candidates(final String methodName) { + final List exact = this.exact.getOrDefault(methodName, List.of()); + if (this.wildcards.isEmpty()) { + return exact; + } + if (exact.isEmpty()) { + return this.wildcards; + } + final List combined = new ArrayList<>(exact.size() + this.wildcards.size()); + combined.addAll(exact); + combined.addAll(this.wildcards); + return combined; + } + + NameIndex toImmutable() { + return new NameIndex( + this.exact.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> List.copyOf(e.getValue()))), + List.copyOf(this.wildcards) + ); + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/action/DirectStaticCall.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/DirectStaticCall.java new file mode 100644 index 0000000..d1f8712 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/DirectStaticCall.java @@ -0,0 +1,100 @@ +package io.papermc.classfile.method.action; + +import io.papermc.classfile.ClassFiles; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.Objects; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +import static io.papermc.classfile.ClassFiles.CONSTRUCTOR_METHOD_NAME; +import static io.papermc.classfile.ClassFiles.adjustForStatic; +import static io.papermc.classfile.ClassFiles.constructorMethodName; + +/** + * A record that enables the rewriting of method invocation instructions by redirecting + * the method call to a static method on another owner. + * This record implements the {@link MethodRewriteAction} interface, providing functionality + * for rewriting both standard invoke instructions and dynamic invocations. + * + * @param newOwner The target class (owner) for the rewritten method call. + * @param staticMethodName The method name to be used if this action represents a constructor call. + * Otherwise, the method name will be {@code}create{type_name}"{@code} + */ +public record DirectStaticCall(ClassDesc newOwner, @Nullable String staticMethodName) implements MethodRewriteAction { + + + public DirectStaticCall(final ClassDesc newOwner) { + this(newOwner, null); + } + + @Override + public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { + return Optional.empty(); + } + + private String constructorStaticMethodName(final ClassDesc owner) { + return Objects.requireNonNullElseGet(this.staticMethodName, () -> constructorMethodName(owner)); + } + + private String staticMethodName(final String originalName) { + if (this.staticMethodName != null) { + return this.staticMethodName; + } + return originalName; + } + + @Override + public void rewriteInvoke(final MethodTransformContext context, final Opcode opcode) { + final MethodTypeDesc descriptor = context.methodInfo().descriptor(); + final ClassDesc owner = context.methodInfo().owner(); + final String name = context.methodInfo().name(); + final MethodTypeDesc newDescriptor = adjustForStatic(opcode, owner, descriptor); + final String newMethodName; + if (opcode == Opcode.INVOKESPECIAL) { + if (CONSTRUCTOR_METHOD_NAME.equals(name)) { + newMethodName = this.constructorStaticMethodName(owner); + } else { + throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); + } + } else { + newMethodName = this.staticMethodName(name); + } + context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.newOwner(), newMethodName, newDescriptor))); + } + + @Override + public void rewriteInvokeDynamic(final MethodTransformContext context, final DirectMethodHandleDesc.Kind kind, final BootstrapInfo bootstrapInfo) { + final MethodTypeDesc descriptor = context.methodInfo().descriptor(); + final ClassDesc owner = context.methodInfo().owner(); + final String name = context.methodInfo().name(); + final MethodTypeDesc newDescriptor = adjustForStatic(kind, owner, descriptor); + final ConstantDesc[] newBootstrapArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); + if (kind == DirectMethodHandleDesc.Kind.INTERFACE_VIRTUAL || kind == DirectMethodHandleDesc.Kind.VIRTUAL) { + newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.staticMethodName(name), newDescriptor); + } else if (kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { + if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { + newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor); + // TODO not really needed on **every** rewrite, just the fuzzy param ones, but it doesn't seem to break anything since it will always be the same + newBootstrapArgs[ClassFiles.DYNAMIC_TYPE_IDX] = newDescriptor; + } else { + throw new UnsupportedOperationException("Unhandled static rewrite: " + kind + " " + owner + " " + name + " " + descriptor); + } + } else if (kind != DirectMethodHandleDesc.Kind.STATIC && kind != DirectMethodHandleDesc.Kind.INTERFACE_STATIC) { + throw new UnsupportedOperationException("Unhandled static rewrite: " + kind + " " + owner + " " + name + " " + descriptor); + } else { + // is a static method + newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.staticMethodName(name), newDescriptor); + } + context.emit(InvokeDynamicInstruction.of(context.constantPool().invokeDynamicEntry(bootstrapInfo.create(newBootstrapArgs)))); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/action/MethodRewriteAction.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/MethodRewriteAction.java new file mode 100644 index 0000000..4c55d4b --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/MethodRewriteAction.java @@ -0,0 +1,54 @@ +package io.papermc.classfile.method.action; + +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.Opcode; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.DynamicCallSiteDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.List; +import java.util.Optional; + +public sealed interface MethodRewriteAction permits DirectStaticCall, SubtypeReturn, SupertypeParam, WrapParamValue, WrapReturnValue { + + /** + * Check that the specified rewrite is configured correctly for this action. + * + * @param namePredicate the name predicate to check + * @param descriptorPredicate the descriptor predicate to check + * @return empty optional if valid, error message otherwise + */ + Optional isValidFor(MethodNamePredicate namePredicate, MethodDescriptorPredicate descriptorPredicate); + + /** + * Rewrites a method invocation instruction, modifying the method owner, + * name, and descriptor, and emits the modified instruction. + * + * @param context The context containing information about the method invocation. + * @param opcode The opcode of the method invocation instruction. + */ + void rewriteInvoke(MethodTransformContext context, Opcode opcode); + + /** + * Rewrites an invokedynamic instruction, modifying its bootstrap method, + * method owner, method name, and method descriptor, then emits the modified instruction. + * The bootstrap method arguments and type are defined in the {@code BootstrapInfo}. + * + * @param context The context containing information about the invokedynamic instruction. + * @param kind The {@code DirectMethodHandleDesc.Kind} indicating the kind of method handle + * associated with the bootstrap method. + * @param bootstrapInfo An instance of {@code BootstrapInfo} containing details about the bootstrap method, + * including its method handle, invocation name and type, and additional arguments. + */ + void rewriteInvokeDynamic(MethodTransformContext context, DirectMethodHandleDesc.Kind kind, BootstrapInfo bootstrapInfo); + + record BootstrapInfo(DirectMethodHandleDesc method, String invocationName, MethodTypeDesc invocationType, List args) { + + DynamicCallSiteDesc create(final ConstantDesc[] newArgs) { + return DynamicCallSiteDesc.of(this.method, this.invocationName, this.invocationType, newArgs); + } + } + +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/action/SubtypeReturn.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/SubtypeReturn.java new file mode 100644 index 0000000..a364ed7 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/SubtypeReturn.java @@ -0,0 +1,54 @@ +package io.papermc.classfile.method.action; + +import io.papermc.classfile.ClassFiles; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.Optional; + +/** + * A {@link MethodRewriteAction} that changes the return type of a method invocation to a subtype, + * without inserting any conversion call. The return type in the method descriptor is updated to + * {@code newReturnType}, and no additional instructions are emitted. This is valid when the new + * API returns a subtype that is assignment-compatible with the old return type. + * + * @param newReturnType The subtype to use as the new return type in the rewritten descriptor. + */ +public record SubtypeReturn(ClassDesc newReturnType) implements MethodRewriteAction { + + @Override + public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { + // only valid if you are search by return type + if (!(descriptorPredicate instanceof MethodDescriptorPredicate.HasReturn)) { + return Optional.of("You must use a return descriptor predicate on " + descriptorPredicate); + } + return Optional.empty(); + } + + @Override + public void rewriteInvoke(final MethodTransformContext context, final Opcode opcode) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final MethodTypeDesc newDescriptor = info.descriptor().changeReturnType(this.newReturnType); + context.emitChangedDescriptor(opcode, newDescriptor); + } + + @Override + public void rewriteInvokeDynamic(final MethodTransformContext context, final DirectMethodHandleDesc.Kind kind, final BootstrapInfo bootstrapInfo) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final ConstantDesc[] newArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); + final MethodTypeDesc handleMethodType = info.descriptor().changeReturnType(this.newReturnType); + newArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(kind, info.owner(), info.name(), handleMethodType); + // we are changing the descriptor directly instead of delegating, so we need to change the dynamic type + if (newArgs[ClassFiles.DYNAMIC_TYPE_IDX] instanceof final MethodTypeDesc instantiatedType) { + newArgs[ClassFiles.DYNAMIC_TYPE_IDX] = instantiatedType.changeReturnType(this.newReturnType); + } + context.emit(InvokeDynamicInstruction.of(context.constantPool().invokeDynamicEntry(bootstrapInfo.create(newArgs)))); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/action/SupertypeParam.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/SupertypeParam.java new file mode 100644 index 0000000..2bebe10 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/SupertypeParam.java @@ -0,0 +1,52 @@ +package io.papermc.classfile.method.action; + +import io.papermc.classfile.ClassFiles; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.Optional; + +import static io.papermc.classfile.ClassFiles.BOOTSTRAP_HANDLE_IDX; +import static io.papermc.classfile.ClassFiles.replaceParameters; +import static java.util.function.Predicate.isEqual; + +public record SupertypeParam(ClassDesc newParamType) implements MethodRewriteAction { + + @Override + public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { + // only valid if you are search by return type + if (!(descriptorPredicate instanceof MethodDescriptorPredicate.HasParameter)) { + return Optional.of("You must use a parameter descriptor predicate on " + descriptorPredicate); + } + return Optional.empty(); + } + + @Override + public void rewriteInvoke(final MethodTransformContext context, final Opcode opcode) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final ClassDesc targetParamType = context.currentRewrite().descriptor().targetType(); + final MethodTypeDesc newDescriptor = replaceParameters(info.descriptor(), isEqual(targetParamType), this.newParamType()); + context.emitChangedDescriptor(opcode, newDescriptor); + } + + @Override + public void rewriteInvokeDynamic(final MethodTransformContext context, final DirectMethodHandleDesc.Kind kind, final BootstrapInfo bootstrapInfo) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final ClassDesc targetParamType = context.currentRewrite().descriptor().targetType(); + final ConstantDesc[] newArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); + final MethodTypeDesc newDescriptor = replaceParameters(info.descriptor(), isEqual(targetParamType), this.newParamType()); + newArgs[BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(kind, info.owner(), info.name(), newDescriptor); + // we are changing the descriptor directly instead of delegating, so we need to change the dynamic type + if (newArgs[ClassFiles.DYNAMIC_TYPE_IDX] instanceof final MethodTypeDesc instantiatedType) { + newArgs[ClassFiles.DYNAMIC_TYPE_IDX] = replaceParameters(instantiatedType, isEqual(targetParamType), this.newParamType()); + } + context.emit(InvokeDynamicInstruction.of(context.constantPool().invokeDynamicEntry(bootstrapInfo.create(newArgs)))); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapParamValue.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapParamValue.java new file mode 100644 index 0000000..4ba9dca --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapParamValue.java @@ -0,0 +1,110 @@ +package io.papermc.classfile.method.action; + +import io.papermc.classfile.generation.ParameterGeneration; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.Optional; + +import static io.papermc.classfile.ClassFiles.BOOTSTRAP_HANDLE_IDX; +import static io.papermc.classfile.ClassFiles.adjustForStatic; +import static io.papermc.classfile.ClassFiles.constructorMethodName; +import static io.papermc.classfile.ClassFiles.emitInvoke; +import static io.papermc.classfile.ClassFiles.replaceParameters; +import static java.util.function.Predicate.isEqual; + +public record WrapParamValue(ClassDesc converterOwner, String converterMethod, ClassDesc newParamType) implements MethodRewriteAction { + + @Override + public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { + if (!(descriptorPredicate instanceof MethodDescriptorPredicate.HasParameter)) { + return Optional.of("You must use a parameter descriptor predicate on " + descriptorPredicate); + } + return Optional.empty(); + } + + @Override + public void rewriteInvoke(final MethodTransformContext context, final Opcode opcode) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final ClassDesc targetParamType = context.currentRewrite().descriptor().targetType(); + final MethodTypeDesc callDesc = replaceParameters(info.descriptor(), isEqual(targetParamType), this.newParamType()); + final MethodTypeDesc staticReplacement = adjustForStatic(opcode, info.owner(), info.descriptor()); + // descriptor for converting the old param type to the new param type + final MethodTypeDesc converterDesc = MethodTypeDesc.of(this.newParamType(), targetParamType); + + final String bridgeMethodName; + if (opcode == Opcode.INVOKESPECIAL) { + bridgeMethodName = constructorMethodName(info.owner()); + } else { + bridgeMethodName = info.name(); + } + final String bridgeName = context.bridges().registerBridge( + info.owner(), + bridgeMethodName, + staticReplacement, + ParameterGeneration.mutating( + cb -> { + if (opcode == Opcode.INVOKESPECIAL) { + cb.new_(info.owner()); + cb.dup(); + } + }, + (paramType, builder) -> { + if (paramType.equals(targetParamType)) { + builder.invokestatic(this.converterOwner(), this.converterMethod(), converterDesc); + } + }), + cb -> emitInvoke(cb, opcode, info, callDesc, true) + ); + + context.emitToBridgeMethod(bridgeName, staticReplacement); + } + + @Override + public void rewriteInvokeDynamic(final MethodTransformContext context, final DirectMethodHandleDesc.Kind kind, final BootstrapInfo bootstrapInfo) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final ClassDesc targetParamType = context.currentRewrite().descriptor().targetType(); + final MethodTypeDesc callDesc = replaceParameters(info.descriptor(), isEqual(targetParamType), this.newParamType()); + final MethodTypeDesc staticReplacement = adjustForStatic(kind, info.owner(), info.descriptor()); + // descriptor for converting the old param type to the new param type + final MethodTypeDesc converterDesc = MethodTypeDesc.of(this.newParamType(), targetParamType); + + final String bridgeMethodName; + if (kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { + bridgeMethodName = constructorMethodName(info.owner()); + } else { + bridgeMethodName = info.name(); + } + final String bridgeName = context.bridges().registerBridge( + info.owner(), + bridgeMethodName, + staticReplacement, + ParameterGeneration.mutating( + cb -> { + if (kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { + cb.new_(info.owner()); + cb.dup(); + } + }, + (paramType, builder) -> { + if (paramType.equals(targetParamType)) { + builder.invokestatic(this.converterOwner(), this.converterMethod(), converterDesc); + } + } + ), + cb -> emitInvoke(cb, kind, info, callDesc, true) + ); + + // Redirect the invokedynamic to the bridge; arg[2] (instantiated type) stays the same + // because the bridge preserves the original return type. + final ConstantDesc[] newArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); + newArgs[BOOTSTRAP_HANDLE_IDX] = context.createBridgeHandle(bridgeName, staticReplacement); + context.emit(InvokeDynamicInstruction.of(context.constantPool().invokeDynamicEntry(bootstrapInfo.create(newArgs)))); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapReturnValue.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapReturnValue.java new file mode 100644 index 0000000..e38c817 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapReturnValue.java @@ -0,0 +1,102 @@ +package io.papermc.classfile.method.action; + +import io.papermc.classfile.generation.ParameterGeneration; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.transform.MethodTransformContext; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.Optional; + +import static io.papermc.classfile.ClassFiles.BOOTSTRAP_HANDLE_IDX; +import static io.papermc.classfile.ClassFiles.adjustForStatic; +import static io.papermc.classfile.ClassFiles.emitInvoke; + +/** + * A {@link MethodRewriteAction} that intercepts a method invocation, emitting a call to the + * updated method (whose return type changed to {@code newReturnType}), followed by a static + * converter call that converts the new return type back to the original return type expected + * by old code. + * + *

For invokedynamic (lambdas/method references), a synthetic bridge method is generated + * in the class being transformed. The bridge calls the updated method and applies the converter, + * then the invokedynamic is redirected to the bridge.

+ * + * @param converterOwner The class that owns the static converter method. + * @param converterMethod The name of the static converter method, which must accept + * {@code newReturnType} and return the original return type. + * @param newReturnType The return type introduced by the new API. + */ +public record WrapReturnValue(ClassDesc converterOwner, String converterMethod, ClassDesc newReturnType) implements MethodRewriteAction { + + @Override + public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { + // only valid if you are search by return type + if (!(descriptorPredicate instanceof MethodDescriptorPredicate.HasReturn)) { + return Optional.of("You must use a return descriptor predicate on " + descriptorPredicate); + } + if (namePredicate instanceof MethodNamePredicate.Constructor) { + return Optional.of("Cannot wrap return value of constructor"); + } + return Optional.empty(); + } + + @Override + public void rewriteInvoke(final MethodTransformContext context, final Opcode opcode) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final MethodTypeDesc callDesc = info.descriptor().changeReturnType(this.newReturnType); + final MethodTypeDesc staticReplacement = adjustForStatic(opcode, info.owner(), info.descriptor()); + // descriptor for converting the new return type to the old type + final MethodTypeDesc converterDesc = MethodTypeDesc.of(info.descriptor().returnType(), this.newReturnType); + + final String bridgeName = context.bridges().registerBridge( + info.owner(), + info.name(), // don't need to handle ctor names, not allowed + staticReplacement, + ParameterGeneration.standard(), + cb -> { + // Invoke the updated method + emitInvoke(cb, opcode, info, callDesc, false); + // Apply the converter + cb.invokestatic(this.converterOwner, this.converterMethod, converterDesc); + cb.areturn(); + } + ); + + context.emitToBridgeMethod(bridgeName, staticReplacement); + } + + @Override + public void rewriteInvokeDynamic(final MethodTransformContext context, final DirectMethodHandleDesc.Kind kind, final BootstrapInfo bootstrapInfo) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + final MethodTypeDesc callDesc = info.descriptor().changeReturnType(this.newReturnType); + final MethodTypeDesc staticReplacement = adjustForStatic(kind, info.owner(), info.descriptor()); + // descriptor for converting the new return type to the old type + final MethodTypeDesc converterDesc = MethodTypeDesc.of(info.descriptor().returnType(), this.newReturnType); + + // Generate a bridge method with same signature as original invocationType. + // Bridge: loads all params, calls the updated method, applies converter, returns. + final String bridgeName = context.bridges().registerBridge( + info.owner(), + info.name(), // don't need to handle ctor names, not allowed + staticReplacement, + ParameterGeneration.standard(), + cb -> { + // Invoke the updated method + emitInvoke(cb, kind, info, callDesc, false); + // Apply the converter + cb.invokestatic(this.converterOwner(), this.converterMethod(), converterDesc); + } + ); + + // Redirect the invokedynamic to the bridge; arg[2] (instantiated type) stays the same + // because the bridge preserves the original return type. + final ConstantDesc[] newArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); + newArgs[BOOTSTRAP_HANDLE_IDX] = context.createBridgeHandle(bridgeName, staticReplacement); + context.emit(InvokeDynamicInstruction.of(context.constantPool().invokeDynamicEntry(bootstrapInfo.create(newArgs)))); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/BridgeMethodRegistry.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/BridgeMethodRegistry.java new file mode 100644 index 0000000..8374f85 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/BridgeMethodRegistry.java @@ -0,0 +1,84 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.generation.ParameterGeneration; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassFile; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.TypeKind; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import static io.papermc.classfile.ClassFiles.GENERATED_PREFIX; +import static io.papermc.classfile.ClassFiles.toInternalName; + +/** + * Collects bridge (synthetic) methods to be generated into the class currently being transformed. + * Bridge methods are used when an invokedynamic instruction (e.g., a lambda or method reference) + * needs an intermediate static method to perform a type conversion. + * + *

One instance is created per class transformation to avoid accumulating state across classes.

+ */ +public final class BridgeMethodRegistry { + + private final Map bridges = new LinkedHashMap<>(); + + /** + * Registers a bridge method to be generated. Returns the name of the registered method, + * which may differ from {@code baseName} if a method with that name already exists. + * + *

The {@code return} will be done automatically, don't include it in the {@code Consumer}.

+ * + * @param owner owner of the method + * @param methodName name of the method + * @param descriptor method descriptor (parameters and return type) + * @param paramGeneration parameter generation helper + * @param body code generator for the method body + * @return the actual name assigned to the bridge method + */ + public String registerBridge(final ClassDesc owner, final String methodName, final MethodTypeDesc descriptor, final ParameterGeneration paramGeneration, final Consumer body) { + final String baseName = GENERATED_PREFIX + toInternalName(owner).replace('/', '_') + '$' + methodName; + String name = baseName; + int counter = 0; + while (this.bridges.containsKey(name)) { + final MethodGen existing = this.bridges.get(name); + if (existing.descriptor().equals(descriptor)) { + // method's with the same descriptor should function the same + return name; + } + name = baseName + "$" + (++counter); + } + final TypeKind returnTypeKind = TypeKind.from(descriptor.returnType()); + final Consumer finalBuilder = builder -> { + paramGeneration.generateParameters(descriptor, builder); + body.accept(builder); + builder.return_(returnTypeKind); + }; + this.bridges.put(name, new MethodGen(descriptor, finalBuilder)); + return name; + } + + /** + * Emits all registered bridge methods into the given class builder. + * Called at the end of the class transformation. + */ + public void emitAll(final ClassBuilder classBuilder) { + for (final Map.Entry entry : this.bridges.entrySet()) { + final MethodGen gen = entry.getValue(); + classBuilder.withMethod( + entry.getKey(), + gen.descriptor(), + ClassFile.ACC_PRIVATE | ClassFile.ACC_STATIC | ClassFile.ACC_SYNTHETIC, + mb -> mb.withCode(gen.body()) + ); + } + } + + public boolean isEmpty() { + return this.bridges.isEmpty(); + } + + private record MethodGen(MethodTypeDesc descriptor, Consumer body) {} +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/ConstructorAwareCodeTransform.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/ConstructorAwareCodeTransform.java new file mode 100644 index 0000000..4b7ba22 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/ConstructorAwareCodeTransform.java @@ -0,0 +1,153 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.ClassFiles; +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.MethodRewriteIndex; +import io.papermc.classfile.transform.TransformContext; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.NewObjectInstruction; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * This is a CodeTransform that is aware of constructors and + * delays writing all instructions between (inclusive) the NEW and INVOKESPECIAL + * that bound complied constructor invocations. This is done so that they + * can be selectively removed if required. + */ +public class ConstructorAwareCodeTransform implements CodeTransform { + + private final Deque bufferStack = new ArrayDeque<>(); + private final MethodRewriteIndex index; + private final CodeTransform fallbackTransform; + private final TransformContext context; + + public ConstructorAwareCodeTransform(final MethodRewriteIndex index, final CodeTransform fallbackTransform, final TransformContext context) { + this.index = index; + this.fallbackTransform = fallbackTransform; + this.context = context; + } + + @Override + public void accept(final CodeBuilder builder, final CodeElement element) { + if (element instanceof NewObjectInstruction) { + // start of a constructor level + final Level level = new Level(); + level.add(element); + this.bufferStack.push(level); + return; + } + + if (!this.bufferStack.isEmpty()) { + // avoid the wrong inspection at the "add" below saying this can be null + final Level peekedLevel = this.bufferStack.peek(); + if (isConstructor(element)) { + // end of a constructor level + final InvokeInstruction invoke = (InvokeInstruction) element; + final Level level = this.bufferStack.pop(); + final MethodTransforms.BoundRewrite boundRewrite = MethodTransforms.setupRewrite(invoke, this.context); + if (boundRewrite == null) { + // should rarely happen, if ever. Only for some different form of LambdaMetafactory call + level.add(element); + return; + } + final List candidates = this.index.constructorCandidates(invoke.owner().asSymbol()); + MethodTransforms.writeFromCandidates( + candidates, + builder.constantPool(), + invoke, + boundRewrite, + el -> { + // only strip out when we know we are writing the changed instruction + level.stripOutBadInstructions(); + level.addDirect(el); + }, + level::add + ); + // the instruction, either original or modified, should always be added to the level by this point + + if (!this.bufferStack.isEmpty()) { + this.bufferStack.peek().addAllFrom(level); + } else { + level.flush(builder, this.fallbackTransform::accept); + } + } else { + peekedLevel.add(element); + } + return; + } + + // anytime we write to the builder, we first need to check that + // we don't need to also rewrite this instruction + this.fallbackTransform.accept(builder, element); + } + + @Override + public void atEnd(final CodeBuilder builder) { + // Drain stack bottom-up + final List remaining = new ArrayList<>(this.bufferStack); + Collections.reverse(remaining); + remaining.forEach(level -> level.flush(builder, this.fallbackTransform::accept)); + this.bufferStack.clear(); + } + + static boolean isConstructor(final CodeElement element) { + if (!(element instanceof final InvokeInstruction invoke)) { + return false; + } + return invoke.opcode() == Opcode.INVOKESPECIAL && invoke.method().name().equalsString(ClassFiles.CONSTRUCTOR_METHOD_NAME); + } + + private sealed interface LevelElement { + + record PassThrough(CodeElement element) implements LevelElement {} + + record Direct(CodeElement element) implements LevelElement {} + } + + private record Level(List elements) { + + Level() { + this(new ArrayList<>()); + } + + void stripOutBadInstructions() { + // we are removing the POP and NEW instructions here (first 2) + this.elements.removeFirst(); + this.elements.removeFirst(); + } + + void add(final CodeElement element) { + this.add(new LevelElement.PassThrough(element)); + } + + void addDirect(final CodeElement element) { + this.add(new LevelElement.Direct(element)); + } + + void add(final LevelElement element) { + this.elements.add(element); + } + + void addAllFrom(final Level other) { + this.elements.addAll(other.elements); + } + + void flush(final CodeBuilder builder, final BiConsumer rewriteInvoke) { + for (final LevelElement element : this.elements) { + switch (element) { + case final LevelElement.PassThrough pass -> rewriteInvoke.accept(builder, pass.element()); + case final LevelElement.Direct direct -> builder.with(direct.element()); + } + } + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContext.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContext.java new file mode 100644 index 0000000..1d6cdbf --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContext.java @@ -0,0 +1,98 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.transform.TransformContext; +import java.lang.classfile.CodeElement; +import java.lang.classfile.Opcode; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.classfile.constantpool.MemberRefEntry; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.function.Consumer; + +public interface MethodTransformContext extends TransformContext { + + static MethodTransformContext create( + final TransformContext context, + final ConstantPoolBuilder constantPool, + final MethodInfo methodInfo, + final Consumer emit, + final MethodRewrite currentRewrite + ) { + return new MethodTransformContextImpl(context.currentClass(), context.bridges(), constantPool, methodInfo, new TrackingConsumer<>(emit), currentRewrite); + } + + default void emitChangedDescriptor(final Opcode opcode, final MethodTypeDesc newDescriptor) { + final MemberRefEntry ref = opcode == Opcode.INVOKEINTERFACE + ? this.constantPool().interfaceMethodRefEntry(this.methodInfo().owner(), this.methodInfo().name(), newDescriptor) + : this.constantPool().methodRefEntry(this.methodInfo().owner(), this.methodInfo().name(), newDescriptor); + this.emit(InvokeInstruction.of(opcode, ref)); + } + + default void emitToBridgeMethod(final String name, final MethodTypeDesc descriptor) { + this.emit(InvokeInstruction.of( + Opcode.INVOKESTATIC, this.constantPool().methodRefEntry(this.currentClass(), name, descriptor) + )); + } + + default DirectMethodHandleDesc createBridgeHandle(final String name, final MethodTypeDesc descriptor) { + return MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + this.currentClass(), + name, + descriptor + ); + } + + /** + * Emits a given {@link CodeElement} for inclusion in the method's bytecode. + * + * @param element the {@link CodeElement} to be emitted. This represents a single + * instruction or other component to be added to the method body. + */ + void emit(CodeElement element); + + /** + * Gets the current method rewrite being applied. + * + * @return the current method rewrite + */ + MethodRewrite currentRewrite(); + + /** + * Provides access to the constant pool builder associated with the current method transformation. + * The constant pool builder enables adding or resolving constant pool entries needed during + * bytecode generation or transformation. + * + * @return the {@link ConstantPoolBuilder} for managing constant pool entries. + */ + ConstantPoolBuilder constantPool(); + + /** + * Retrieves information about a specific method, combining metadata associated + * with method invocation instructions such as INVOKE and INVOKEDYNAMIC. + * + * @return a {@link MethodInfo} record representing the method's owner, + * name, and descriptor, providing essential details for method matching + * during bytecode transformation or analysis. + */ + MethodInfo methodInfo(); + + /** + * This is just for method matching, combining method information from both + * INVOKE* and INVOKEDYNAMIC instructions. + */ + record MethodInfo(ClassDesc owner, String name, MethodTypeDesc descriptor, boolean isInterface) { + } + + /** + * Call this in a {@code try-with-resources} block to make sure the {@link #emit(CodeElement)} + * is actually called. + * + * @return an {@link AutoCloseable} for a {@code try-with-resources} block + */ + TrackingConsumer.Instance prime(); +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContextImpl.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContextImpl.java new file mode 100644 index 0000000..38a1df3 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContextImpl.java @@ -0,0 +1,26 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.method.MethodRewrite; +import java.lang.classfile.CodeElement; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.constant.ClassDesc; + +record MethodTransformContextImpl( + ClassDesc currentClass, + BridgeMethodRegistry bridges, + ConstantPoolBuilder constantPool, + MethodInfo methodInfo, + TrackingConsumer emit, + MethodRewrite currentRewrite +) implements MethodTransformContext { + + @Override + public void emit(final CodeElement element) { + this.emit.accept(element); + } + + @Override + public TrackingConsumer.Instance prime() { + return this.emit.prime(); + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransforms.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransforms.java new file mode 100644 index 0000000..f77cc1d --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransforms.java @@ -0,0 +1,90 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.transform.TransformContext; +import java.lang.classfile.CodeElement; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.classfile.instruction.InvokeDynamicInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.List; +import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + +import static io.papermc.classfile.ClassFiles.LAMBDA_METAFACTORY; + +public final class MethodTransforms { + + private MethodTransforms() { + } + + static void writeFromCandidates(final List candidates, final ConstantPoolBuilder poolBuilder, final CodeElement element, final BoundRewrite boundRewrite, final Consumer emitter) { + writeFromCandidates(candidates, poolBuilder, element, boundRewrite, emitter, emitter); + } + + static void writeFromCandidates(final List candidates, final ConstantPoolBuilder poolBuilder, final CodeElement element, final BoundRewrite boundRewrite, final Consumer rewriteEmitter, final Consumer originalEmitter) { + boolean written = false; + for (final MethodRewrite candidate : candidates) { + written = boundRewrite.tryWrite(rewriteEmitter, poolBuilder, candidate); + if (written) { + break; + } + } + if (!written) { + originalEmitter.accept(element); + } + } + + static @Nullable BoundRewrite setupRewrite(final CodeElement element, final TransformContext context) { + final ClassDesc owner; + final String methodName; + final MethodTypeDesc descriptor; + final boolean isInterface; + final Writer rewriter; + if (element instanceof final InvokeInstruction invoke) { + owner = invoke.owner().asSymbol(); + methodName = invoke.name().stringValue(); + descriptor = invoke.typeSymbol(); + isInterface = invoke.isInterface(); + rewriter = (methodContext, rewrite) -> rewrite.transformInvoke(methodContext, invoke.opcode()); + } else if (element instanceof final InvokeDynamicInstruction invokeDynamic) { + final DirectMethodHandleDesc bootstrapMethod = invokeDynamic.bootstrapMethod(); + final List args = invokeDynamic.bootstrapArgs(); + if (!bootstrapMethod.owner().equals(LAMBDA_METAFACTORY) || args.size() < 2) { + // only looking for lambda metafactory calls + return null; + } + if (!(args.get(1) instanceof final DirectMethodHandleDesc methodHandle)) { + return null; + } + owner = methodHandle.owner(); + methodName = methodHandle.methodName(); + // we parse a descriptor ourselves that is the real method, not including the receiver for virtual/interface methods + descriptor = MethodTypeDesc.ofDescriptor(methodHandle.lookupDescriptor()); + isInterface = methodHandle.isOwnerInterface(); + rewriter = (methodContext, rewrite) -> rewrite.transformInvokeDynamic(methodContext, bootstrapMethod, methodHandle, args, invokeDynamic); + } else { + return null; + } + final MethodTransformContext.MethodInfo info = new MethodTransformContext.MethodInfo(owner, methodName, descriptor, isInterface); + return new BoundRewrite(rewriter, info, context); + } + + record BoundRewrite(Writer writer, MethodTransformContext.MethodInfo methodInfo, TransformContext context) { + + public boolean tryWrite(final Consumer emit, final ConstantPoolBuilder poolBuilder, final MethodRewrite methodRewrite) { + final TrackingConsumer checkedEmit = new TrackingConsumer<>(emit); + final MethodTransformContext methodContext = MethodTransformContext.create(this.context, poolBuilder, this.methodInfo, checkedEmit, methodRewrite); + return this.writer.write(methodContext, methodRewrite); + } + } + + @FunctionalInterface + interface Writer { + boolean write(MethodTransformContext context, MethodRewrite rewrite); + } + +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/SimpleMethodBodyTransform.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/SimpleMethodBodyTransform.java new file mode 100644 index 0000000..d97f4da --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/SimpleMethodBodyTransform.java @@ -0,0 +1,54 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.MethodRewriteIndex; +import io.papermc.classfile.transform.TransformContext; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeInstruction; +import java.util.List; + +/** + * This is a transform for rewriting all non-INVOKESPECIAL instructions. + * Method constructors that aren't lambdas require iterating over the full + * method body to remove earlier instructions. Use {@link ConstructorAwareCodeTransform} + * for that. + */ +public class SimpleMethodBodyTransform implements CodeTransform { + + private final MethodRewriteIndex index; + private final TransformContext context; + + public SimpleMethodBodyTransform(final MethodRewriteIndex index, final TransformContext context) { + this.index = index; + this.context = context; + } + + @Override + public void accept(final CodeBuilder builder, final CodeElement element) { + final MethodTransforms.BoundRewrite boundRewrite = MethodTransforms.setupRewrite(element, this.context); + if (boundRewrite == null) { + builder.with(element); + return; + } + final List candidates = this.index.candidates(boundRewrite.methodInfo().owner(), boundRewrite.methodInfo().name()); + final boolean checkInvokeSpecial = element instanceof final InvokeInstruction invoke && invoke.opcode() == Opcode.INVOKESPECIAL; + MethodTransforms.writeFromCandidates( + candidates, + builder.constantPool(), + element, + boundRewrite, + el -> { + // guard against making INVOKESPECIAL changes here + if (checkInvokeSpecial) { + throw new UnsupportedOperationException("Cannot make INVOKESPECIAL instruction changes here"); + } + builder.with(el); + }, + builder::with + ); + + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/method/transform/TrackingConsumer.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/TrackingConsumer.java new file mode 100644 index 0000000..d007074 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/TrackingConsumer.java @@ -0,0 +1,40 @@ +package io.papermc.classfile.method.transform; + +import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + +public final class TrackingConsumer implements Consumer { + + private final Consumer wrapped; + @Nullable Instance primed = null; + boolean called = false; + + TrackingConsumer(final Consumer wrapped) { + this.wrapped = wrapped; + } + + @Override + public void accept(final T t) { + this.wrapped.accept(t); + this.called = true; + } + + public Instance prime() { + this.primed = new Instance(); + return this.primed; + } + + private void verify() { + if (!this.called) { + throw new IllegalStateException("Consumer was not called"); + } + } + + public final class Instance implements AutoCloseable { + + @Override + public void close() { + TrackingConsumer.this.verify(); + } + } +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/package-info.java b/classfile-utils/src/main/java/io/papermc/classfile/package-info.java new file mode 100644 index 0000000..2eceb4a --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.papermc.classfile; + +import org.jspecify.annotations.NullMarked; diff --git a/classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContext.java b/classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContext.java new file mode 100644 index 0000000..7fabe54 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContext.java @@ -0,0 +1,15 @@ +package io.papermc.classfile.transform; + +import io.papermc.classfile.method.transform.BridgeMethodRegistry; +import java.lang.constant.ClassDesc; + +public interface TransformContext { + + static TransformContext create(final ClassDesc currentClass, final BridgeMethodRegistry bridges) { + return new TransformContextImpl(currentClass, bridges); + } + + ClassDesc currentClass(); + + BridgeMethodRegistry bridges(); +} diff --git a/classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContextImpl.java b/classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContextImpl.java new file mode 100644 index 0000000..db4f6a5 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContextImpl.java @@ -0,0 +1,7 @@ +package io.papermc.classfile.transform; + +import io.papermc.classfile.method.transform.BridgeMethodRegistry; +import java.lang.constant.ClassDesc; + +record TransformContextImpl(ClassDesc currentClass, BridgeMethodRegistry bridges) implements TransformContext { +} diff --git a/classfile-utils/src/mainForNewTargets/java/io/papermc/asm/rules/classes/LegacyEnum.java b/classfile-utils/src/mainForNewTargets/java/io/papermc/asm/rules/classes/LegacyEnum.java new file mode 100644 index 0000000..3063548 --- /dev/null +++ b/classfile-utils/src/mainForNewTargets/java/io/papermc/asm/rules/classes/LegacyEnum.java @@ -0,0 +1,30 @@ +package io.papermc.asm.rules.classes; + +import java.util.Optional; + +/** + * This type needs to be implemented by the implementation of the + * type that was previously an enum and is now an interface. + * + *

+ * Types need to have static methods for {@code values()} + * and {@code valueOf(String)}. + *

+ * + * @param the implementation type + */ +public interface LegacyEnum> extends Comparable { + + String name(); + + int ordinal(); + + @SuppressWarnings({"unchecked", "MethodName"}) + default Class getDeclaringClass() { + return (Class) this.getClass(); + } + + default Optional> describeConstable() { + return this.getDeclaringClass().describeConstable().map(c -> Enum.EnumDesc.of(c, this.name())); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/TestUtil.java b/classfile-utils/src/test/java/io/papermc/classfile/TestUtil.java new file mode 100644 index 0000000..6ee9797 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/TestUtil.java @@ -0,0 +1,216 @@ +package io.papermc.classfile; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.classfile.Attributes; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.attribute.InnerClassInfo; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.spi.ToolProvider; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class TestUtil { + private TestUtil() { + } + + private static final ToolProvider JAVAP_PROVIDER = ToolProvider.findFirst("javap").orElseThrow(() -> new IllegalStateException("javap not found")); + + public static Map inputBytes(final String className) { + return readClassBytes(new HashMap<>(), className, n -> n + ".class"); + } + + public interface Processor { + byte[] process(byte[] bytes) throws E; + } + + public record DefaultProcessor(RewriteProcessor rewriteProcessor) implements Processor { + @Override + public byte[] process(final byte[] bytes) { + return this.rewriteProcessor.rewrite(bytes); + } + } + + public static void assertProcessedMatchesExpected(final String className, final RewriteProcessor rewriteProcessor) { + assertProcessedMatchesExpected_(className, new DefaultProcessor(rewriteProcessor)); + } + + private static boolean checkJavapDiff(final String name, final byte[] expected, final byte[] processed, final List javapArgs) { + final String[] command = new String[javapArgs.size() + 1]; + for (int i = 0; i < javapArgs.size(); i++) { + command[i] = javapArgs.get(i); + } + try { + Path tmp = Files.createTempDirectory("tmpasmutils"); + Path cls = tmp.resolve("cls.class"); + Files.write(cls, expected); + command[javapArgs.size()] = cls.toAbsolutePath().toString(); + + final StringWriter expectedStringWriter = new StringWriter(); + final PrintWriter expectedWriter = new PrintWriter(expectedStringWriter); + JAVAP_PROVIDER.run(expectedWriter, expectedWriter, command); + final String expectedJavap = expectedStringWriter.toString(); + + tmp = Files.createTempDirectory("tmpasmutils"); + cls = tmp.resolve("cls.class"); + Files.write(cls, processed); + command[javapArgs.size()] = cls.toAbsolutePath().toString(); + final StringWriter actualStringWriter = new StringWriter(); + final PrintWriter actualWriter = new PrintWriter(actualStringWriter); + JAVAP_PROVIDER.run(actualWriter, actualWriter, command); + final String actualJavap = actualStringWriter.toString(); + + assertEquals(expectedJavap, actualJavap, () -> "Transformed class bytes did not match expected for " + name + ".class"); + } catch (final IOException exception) { + exception.printStackTrace(); + System.err.println("Failed to diff class bytes using javap, falling back to direct byte comparison."); + return false; + } + return true; + } + + private static void assertProcessedMatchesExpected_( + final String className, + final Processor processor + ) { + final Map input = inputBytes(className.replace(".", "/")); + final Map processed = processClassBytes(input, processor); + final Map expected; + try { + expected = expectedBytes(className.replace(".", "/")); + } catch (final RuntimeException e) { + if (e.getCause() instanceof FileNotFoundException) { + final Path expectedDir = Path.of("src/testData/resources/expected"); + for (final Map.Entry entry : processed.entrySet()) { + final Path outPath = expectedDir.resolve(entry.getKey() + ".class"); + if (Files.exists(outPath)) { + throw new IllegalStateException(); + } + try { + Files.createDirectories(outPath.getParent()); + Files.write(outPath, entry.getValue()); + } catch (final IOException ex0) { + throw new RuntimeException(ex0); + } + } + throw new RuntimeException("Expected data not present, wrote current processed output."); + } + throw e; + } + for (final String name : input.keySet()) { + if (Arrays.equals(expected.get(name), processed.get(name))) { + // Bytes equal + return; + } else { + // Try to get a javap diff + // final boolean proceed = checkJavapDiff(name, expected.get(name), processed.get(name), Arrays.asList(JAVAP_PATH, "-c", "-p")); + // verbose is too useful for invokedynamic debugging to omit + checkJavapDiff(name, expected.get(name), processed.get(name), Arrays.asList("-c", "-p", "-v")); + + // If javap failed, just assert the bytes equal + assertArrayEquals( + expected.get(name), + processed.get(name), + () -> "Transformed class bytes did not match expected for " + name + ".class" + ); + } + } + } + + @SuppressWarnings({"RedundantCast", "unchecked"}) + public static Map processClassBytes( + final Map input, + final Processor proc + ) { + final Map output = new HashMap<>(input.size()); + for (final Map.Entry entry : input.entrySet()) { + output.put(entry.getKey(), ((Processor) proc).process(entry.getValue())); + } + return output; + } + + public static Map expectedBytes(final String className) { + return readClassBytes(new HashMap<>(), className, n -> "expected/" + n + ".class"); + } + + private static Map readClassBytes( + final Map map, + final String className, + final Function classNameMapper + ) { + try { + final URL url = TestUtil.class.getClassLoader().getResource(classNameMapper.apply(className)); + if (url == null) { + throw new FileNotFoundException(classNameMapper.apply(className)); + } + final InputStream s = url.openStream(); + try (s) { + final byte[] rootBytes = s.readAllBytes(); + final ClassModel model = ClassFile.of().parse(rootBytes); + final String thisName = model.thisClass().asInternalName(); + map.put(thisName, rootBytes); + model.findAttribute(Attributes.innerClasses()).ifPresent(attr -> { + for (final InnerClassInfo info : attr.classes()) { + if (info.outerClass().isEmpty()) continue; + if (!info.outerClass().get().asInternalName().equals(thisName)) continue; + readClassBytes(map, info.innerClass().asInternalName(), classNameMapper); + } + }); + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + return map; + } + + public static void processAndExecute( + final String className, + final Processor proc + ) { + processAndExecute(className, proc, "entry"); + } + + public static void processAndExecute( + final String className, + final Processor proc, + final String methodName + ) { + final Map input = TestUtil.inputBytes(className.replace(".", "/")); + final Map processed = TestUtil.processClassBytes(input, proc); + + final var loader = new URLClassLoader(new URL[]{}, TestUtil.class.getClassLoader()) { + @Override + protected Class findClass(final String name) throws ClassNotFoundException { + final String slashName = name.replace(".", "/"); + final byte[] processedBytes = processed.get(slashName); + if (processedBytes != null) { + return super.defineClass(name, processedBytes, 0, processedBytes.length); + } + return super.findClass(name); + } + }; + + try { + final Class loaded = loader.findClass(className); + final Method main = loaded.getDeclaredMethod(methodName); + main.trySetAccessible(); + main.invoke(null); + } catch (final ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/TransformerTest.java b/classfile-utils/src/test/java/io/papermc/classfile/TransformerTest.java new file mode 100644 index 0000000..0b5c3fb --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/TransformerTest.java @@ -0,0 +1,21 @@ +package io.papermc.classfile; + +import io.papermc.classfile.checks.TransformerChecksProvider; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@ParameterizedTest(name = "{arguments}") +@ArgumentsSource(TransformerChecksProvider.class) +public @interface TransformerTest { + @Language("jvm-class-name") + String value(); +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/checks/ExecutionTransformerCheck.java b/classfile-utils/src/test/java/io/papermc/classfile/checks/ExecutionTransformerCheck.java new file mode 100644 index 0000000..858bac2 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/checks/ExecutionTransformerCheck.java @@ -0,0 +1,12 @@ +package io.papermc.classfile.checks; + +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TestUtil; + +public record ExecutionTransformerCheck(String className) implements TransformerCheck { + + @Override + public void run(final RewriteProcessor rewrite) { + TestUtil.processAndExecute(this.className, new TestUtil.DefaultProcessor(rewrite)); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/checks/RewriteTransformerCheck.java b/classfile-utils/src/test/java/io/papermc/classfile/checks/RewriteTransformerCheck.java new file mode 100644 index 0000000..387174f --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/checks/RewriteTransformerCheck.java @@ -0,0 +1,12 @@ +package io.papermc.classfile.checks; + +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TestUtil; + +public record RewriteTransformerCheck(String className) implements TransformerCheck { + + @Override + public void run(final RewriteProcessor rewriteProcessor) { + TestUtil.assertProcessedMatchesExpected(this.className, rewriteProcessor); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerCheck.java b/classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerCheck.java new file mode 100644 index 0000000..51bac09 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerCheck.java @@ -0,0 +1,8 @@ +package io.papermc.classfile.checks; + +import io.papermc.classfile.RewriteProcessor; + +public interface TransformerCheck { + + void run(RewriteProcessor rule); +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerChecksProvider.java b/classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerChecksProvider.java new file mode 100644 index 0000000..e3b311c --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerChecksProvider.java @@ -0,0 +1,21 @@ +package io.papermc.classfile.checks; + +import io.papermc.classfile.TransformerTest; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.support.AnnotationSupport; + +public class TransformerChecksProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(final ParameterDeclarations parameters, final ExtensionContext context) { + final TransformerTest test = AnnotationSupport.findAnnotation(context.getTestMethod(), TransformerTest.class).orElseThrow(); + return Stream.of( + Arguments.of(new RewriteTransformerCheck(test.value())), + Arguments.of(new ExecutionTransformerCheck(test.value())) + ); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/checks/package-info.java b/classfile-utils/src/test/java/io/papermc/classfile/checks/package-info.java new file mode 100644 index 0000000..bd2361f --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/checks/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.papermc.classfile.checks; + +import org.jspecify.annotations.NullMarked; diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodDescriptorPredicate.java b/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodDescriptorPredicate.java new file mode 100644 index 0000000..3aeb769 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodDescriptorPredicate.java @@ -0,0 +1,76 @@ +package io.papermc.classfile.method; + +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.lang.constant.MethodTypeDesc; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestMethodDescriptorPredicate { + + static final ClassDesc STRING = ClassDesc.of("java.lang.String"); + static final ClassDesc INTEGER = ClassDesc.of("java.lang.Integer"); + static final ClassDesc OBJECT = ClassDesc.of("java.lang.Object"); + + @Test + void hasReturnMatchesWhenReturnTypeMatches() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasReturn(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(STRING, OBJECT); + assertThat(predicate.test(desc)).isTrue(); + } + + @Test + void hasReturnNoMatchWhenReturnTypeDiffers() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasReturn(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(OBJECT, STRING); + assertThat(predicate.test(desc)).isFalse(); + } + + @Test + void hasReturnNoMatchVoidReturn() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasReturn(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(ConstantDescs.CD_void, STRING); + assertThat(predicate.test(desc)).isFalse(); + } + + @Test + void hasReturnTargetTypeIsReturnType() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasReturn(STRING); + assertThat(predicate.targetType()).isEqualTo(STRING); + } + + @Test + void hasParameterMatchesWhenParamPresent() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasParameter(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(ConstantDescs.CD_void, INTEGER, STRING, OBJECT); + assertThat(predicate.test(desc)).isTrue(); + } + + @Test + void hasParameterMatchesSingleParam() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasParameter(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(ConstantDescs.CD_void, STRING); + assertThat(predicate.test(desc)).isTrue(); + } + + @Test + void hasParameterNoMatchWhenParamAbsent() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasParameter(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(ConstantDescs.CD_void, INTEGER, OBJECT); + assertThat(predicate.test(desc)).isFalse(); + } + + @Test + void hasParameterNoMatchNoParams() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasParameter(STRING); + final MethodTypeDesc desc = MethodTypeDesc.of(ConstantDescs.CD_void); + assertThat(predicate.test(desc)).isFalse(); + } + + @Test + void hasParameterTargetTypeIsParamType() { + final MethodDescriptorPredicate predicate = MethodDescriptorPredicate.hasParameter(STRING); + assertThat(predicate.targetType()).isEqualTo(STRING); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodNamePredicate.java b/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodNamePredicate.java new file mode 100644 index 0000000..4f42954 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodNamePredicate.java @@ -0,0 +1,90 @@ +package io.papermc.classfile.method; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TestMethodNamePredicate { + + @Test + void exactMatchSingleNameMatchesExact() { + final MethodNamePredicate predicate = MethodNamePredicate.exact("doThing"); + assertThat(predicate.test("doThing")).isTrue(); + } + + @Test + void exactMatchSingleNameNoMatchOther() { + final MethodNamePredicate predicate = MethodNamePredicate.exact("doThing"); + assertThat(predicate.test("doOtherThing")).isFalse(); + } + + @Test + void exactMatchMultipleNamesMatchesAny() { + final MethodNamePredicate predicate = MethodNamePredicate.exact("doThing", "doOtherThing"); + assertThat(predicate.test("doThing")).isTrue(); + assertThat(predicate.test("doOtherThing")).isTrue(); + } + + @Test + void exactMatchMultipleNamesNoMatchUnknown() { + final MethodNamePredicate predicate = MethodNamePredicate.exact("doThing", "doOtherThing"); + assertThat(predicate.test("doThirdThing")).isFalse(); + } + + @Test + void exactMatchRejectsInitMethodName() { + assertThatThrownBy(() -> MethodNamePredicate.exact("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void prefixMatchMatchesPrefix() { + final MethodNamePredicate predicate = MethodNamePredicate.prefix("get"); + assertThat(predicate.test("getEntity")).isTrue(); + assertThat(predicate.test("getName")).isTrue(); + } + + @Test + void prefixMatchNoMatchDifferentPrefix() { + final MethodNamePredicate predicate = MethodNamePredicate.prefix("get"); + assertThat(predicate.test("setEntity")).isFalse(); + assertThat(predicate.test("get")).isTrue(); // exact prefix match also counts + } + + @Test + void prefixMatchNoMatchShorterThanPrefix() { + final MethodNamePredicate predicate = MethodNamePredicate.prefix("getEntity"); + assertThat(predicate.test("get")).isFalse(); + } + + @Test + void prefixMatchRejectsInitPrefix() { + assertThatThrownBy(() -> MethodNamePredicate.prefix("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void prefixMatchRejectsPartialInitPrefix() { + assertThatThrownBy(() -> MethodNamePredicate.prefix("")).isTrue(); + } + + @Test + void constructorNoMatchNonInit() { + final MethodNamePredicate predicate = MethodNamePredicate.constructor(); + assertThat(predicate.test("doThing")).isFalse(); + assertThat(predicate.test("init")).isFalse(); + } + + @Test + void constructorReturnsSingleton() { + assertThat(MethodNamePredicate.constructor()).isSameAs(MethodNamePredicate.constructor()); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodRewriteIndex.java b/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodRewriteIndex.java new file mode 100644 index 0000000..139c5f2 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodRewriteIndex.java @@ -0,0 +1,184 @@ +package io.papermc.classfile.method; + +import io.papermc.classfile.ClassFiles; +import io.papermc.classfile.method.action.DirectStaticCall; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.util.List; +import org.junit.jupiter.api.Test; + +import static io.papermc.classfile.method.MethodDescriptorPredicate.hasReturn; +import static io.papermc.classfile.method.MethodNamePredicate.constructor; +import static io.papermc.classfile.method.MethodNamePredicate.exact; +import static io.papermc.classfile.method.MethodNamePredicate.prefix; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class TestMethodRewriteIndex { + + private static final ClassDesc OWNER_A = ClassDesc.of("com.example.A"); + private static final ClassDesc OWNER_B = ClassDesc.of("com.example.B"); + + private static MethodRewrite exactRewrite(final ClassDesc owner, final String methodName) { + return new MethodRewrite(owner, exact(methodName), hasReturn(ConstantDescs.CD_void), mock(DirectStaticCall.class)); + } + + private static MethodRewrite wildcardRewrite(final ClassDesc owner) { + return new MethodRewrite(owner, prefix("get"), hasReturn(ConstantDescs.CD_void), mock(DirectStaticCall.class)); + } + + private static MethodRewrite constructorRewrite(final ClassDesc owner) { + return new MethodRewrite(owner, constructor(), hasReturn(ConstantDescs.CD_void), mock(DirectStaticCall.class)); + } + + @Test + void noRewritesReturnsEmpty() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of()); + assertThat(index.candidates(OWNER_A, "foo")).isEmpty(); + } + + @Test + void exactMatchReturnsRewrite() { + final MethodRewrite rewrite = exactRewrite(OWNER_A, "foo"); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(rewrite)); + assertThat(index.candidates(OWNER_A, "foo")).containsExactly(rewrite); + } + + @Test + void exactMatchWrongNameReturnsEmpty() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(exactRewrite(OWNER_A, "foo"))); + assertThat(index.candidates(OWNER_A, "bar")).isEmpty(); + } + + @Test + void exactMatchWrongOwnerReturnsEmpty() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(exactRewrite(OWNER_A, "foo"))); + assertThat(index.candidates(OWNER_B, "foo")).isEmpty(); + } + + @Test + void wildcardMatchesByOwnerRegardlessOfMethodName() { + final MethodRewrite rewrite = wildcardRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(rewrite)); + assertThat(index.candidates(OWNER_A, "foo")).containsExactly(rewrite); + assertThat(index.candidates(OWNER_A, "bar")).containsExactly(rewrite); + } + + @Test + void wildcardWrongOwnerReturnsEmpty() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(wildcardRewrite(OWNER_A))); + assertThat(index.candidates(OWNER_B, "foo")).isEmpty(); + } + + @Test + void exactAndWildcardForSameOwnerBothReturned() { + final MethodRewrite exact = exactRewrite(OWNER_A, "foo"); + final MethodRewrite wildcard = wildcardRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(exact, wildcard)); + assertThat(index.candidates(OWNER_A, "foo")).containsExactly(exact, wildcard); + } + + @Test + void exactDoesNotReturnForNonMatchingNameWhenWildcardPresent() { + final MethodRewrite exact = exactRewrite(OWNER_A, "foo"); + final MethodRewrite wildcard = wildcardRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(exact, wildcard)); + // wildcard matches, exact does not + assertThat(index.candidates(OWNER_A, "bar")).containsExactly(wildcard); + } + + @Test + void multipleExactMatchesSameOwnerAndName() { + final MethodRewrite first = exactRewrite(OWNER_A, "foo"); + final MethodRewrite second = exactRewrite(OWNER_A, "foo"); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(first, second)); + assertThat(index.candidates(OWNER_A, "foo")).containsExactly(first, second); + } + + @Test + void differentOwnersDoNotInterfere() { + final MethodRewrite rewriteA = exactRewrite(OWNER_A, "foo"); + final MethodRewrite rewriteB = exactRewrite(OWNER_B, "foo"); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(rewriteA, rewriteB)); + assertThat(index.candidates(OWNER_A, "foo")).containsExactly(rewriteA); + assertThat(index.candidates(OWNER_B, "foo")).containsExactly(rewriteB); + } + + @Test + void orderIsPreservedExactBeforeWildcard() { + final MethodRewrite exact = exactRewrite(OWNER_A, "foo"); + final MethodRewrite wildcard = wildcardRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(exact, wildcard)); + final List candidates = index.candidates(OWNER_A, "foo"); + assertThat(candidates.indexOf(exact)).isLessThan(candidates.indexOf(wildcard)); + } + + // --- hasConstructorRewrites --- + + @Test + void hasConstructorRewritesReturnsFalseWhenEmpty() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of()); + assertThat(index.hasConstructorRewrites()).isFalse(); + } + + @Test + void hasConstructorRewritesReturnsFalseWhenOnlyNonConstructorRewrites() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(exactRewrite(OWNER_A, "foo"), wildcardRewrite(OWNER_B))); + assertThat(index.hasConstructorRewrites()).isFalse(); + } + + @Test + void hasConstructorRewritesReturnsTrueWhenConstructorRewritePresent() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(constructorRewrite(OWNER_A))); + assertThat(index.hasConstructorRewrites()).isTrue(); + } + + // --- constructorCandidates --- + + @Test + void constructorCandidatesReturnsEmptyWhenNoneRegistered() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of()); + assertThat(index.constructorCandidates(OWNER_A)).isEmpty(); + } + + @Test + void constructorCandidatesReturnsEmptyForNonMatchingOwner() { + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(constructorRewrite(OWNER_A))); + assertThat(index.constructorCandidates(OWNER_B)).isEmpty(); + } + + @Test + void constructorCandidatesReturnsRewriteForMatchingOwner() { + final MethodRewrite ctor = constructorRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(ctor)); + assertThat(index.constructorCandidates(OWNER_A)).containsExactly(ctor); + } + + @Test + void constructorCandidatesMultipleRewritesSameOwnerAllReturned() { + final MethodRewrite first = constructorRewrite(OWNER_A); + final MethodRewrite second = constructorRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(first, second)); + assertThat(index.constructorCandidates(OWNER_A)).containsExactly(first, second); + } + + @Test + void constructorCandidatesDifferentOwnersDontInterfere() { + final MethodRewrite ctorA = constructorRewrite(OWNER_A); + final MethodRewrite ctorB = constructorRewrite(OWNER_B); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(ctorA, ctorB)); + assertThat(index.constructorCandidates(OWNER_A)).containsExactly(ctorA); + assertThat(index.constructorCandidates(OWNER_B)).containsExactly(ctorB); + } + + // --- constructor rewrite interaction with regular candidates --- + + @Test + void constructorRewriteAppearsInCandidatesAsWildcard() { + // Constructor rewrites fall through to the wildcard slot in the regular index + final MethodRewrite ctor = constructorRewrite(OWNER_A); + final MethodRewriteIndex index = new MethodRewriteIndex(List.of(ctor)); + assertThat(index.candidates(OWNER_A, ClassFiles.CONSTRUCTOR_METHOD_NAME)).containsExactly(ctor); + assertThat(index.candidates(OWNER_A, "anyOtherMethod")).containsExactly(ctor); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestDirectStaticCall.java b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestDirectStaticCall.java new file mode 100644 index 0000000..27d6542 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestDirectStaticCall.java @@ -0,0 +1,43 @@ +package io.papermc.classfile.method.action; + +import data.methods.Methods; +import data.methods.Redirects; +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TransformerTest; +import io.papermc.classfile.checks.TransformerCheck; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.MethodRewrite; +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.List; + +import static io.papermc.classfile.ClassFiles.desc; + +class TestDirectStaticCall { + + static final ClassDesc PLAYER = desc(Player.class); + static final ClassDesc ENTITY = desc(Entity.class); + static final ClassDesc METHODS_WRAPPER = desc(Methods.Wrapper.class); + + static final ClassDesc NEW_OWNER = desc(Redirects.class); + + @TransformerTest("data.methods.statics.PlainUser") + void test(final TransformerCheck check) { + final List rewriteList = new ArrayList<>(); + final List methodNames = List.of("addEntity", "addEntityStatic", "addEntityAndPlayer", "addEntityAndPlayerStatic"); + for (final String methodName : methodNames) { + rewriteList.add(new MethodRewrite(PLAYER, MethodNamePredicate.exact(methodName), MethodDescriptorPredicate.hasParameter(ENTITY), new DirectStaticCall(NEW_OWNER))); + } + final List entityMethodNames = List.of("setOwner"); + for (final String entityMethodName : entityMethodNames) { + rewriteList.add(new MethodRewrite(ENTITY, MethodNamePredicate.exact(entityMethodName), MethodDescriptorPredicate.hasParameter(ENTITY), new DirectStaticCall(NEW_OWNER))); + } + + rewriteList.add(new MethodRewrite(METHODS_WRAPPER, MethodNamePredicate.constructor(), MethodDescriptorPredicate.hasParameter(PLAYER), new DirectStaticCall(NEW_OWNER))); + final RewriteProcessor rewriteProcessor = new RewriteProcessor(rewriteList); + check.run(rewriteProcessor); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSubtypeReturn.java b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSubtypeReturn.java new file mode 100644 index 0000000..a744776 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSubtypeReturn.java @@ -0,0 +1,32 @@ +package io.papermc.classfile.method.action; + +import data.methods.Methods; +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TransformerTest; +import io.papermc.classfile.checks.TransformerCheck; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.MethodRewrite; +import java.lang.constant.ClassDesc; +import java.util.List; + +import static io.papermc.classfile.ClassFiles.desc; +import static io.papermc.classfile.method.MethodDescriptorPredicate.hasReturn; +import static io.papermc.classfile.method.MethodNamePredicate.exact; + +class TestSubtypeReturn { + + static final ClassDesc METHODS = desc(Methods.class); + static final ClassDesc ENTITY = desc(Entity.class); + static final ClassDesc PLAYER = desc(Player.class); + + @TransformerTest("data.methods.inplace.SubTypeReturnUser") + void test(final TransformerCheck check) { + final List rewrites = List.of( + new MethodRewrite(METHODS, exact("get", "getStatic"), hasReturn(ENTITY), new SubtypeReturn(PLAYER)) + ); + check.run(new RewriteProcessor(rewrites)); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSupertypeParam.java b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSupertypeParam.java new file mode 100644 index 0000000..656d681 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSupertypeParam.java @@ -0,0 +1,34 @@ +package io.papermc.classfile.method.action; + +import data.methods.Methods; +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TransformerTest; +import io.papermc.classfile.checks.TransformerCheck; +import io.papermc.classfile.method.MethodRewrite; +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.List; + +import static io.papermc.classfile.ClassFiles.desc; +import static io.papermc.classfile.method.MethodDescriptorPredicate.hasParameter; +import static io.papermc.classfile.method.MethodNamePredicate.exact; + +class TestSupertypeParam { + + static final ClassDesc METHODS = desc(Methods.class); + static final ClassDesc PLAYER = desc(Player.class); + static final ClassDesc ENTITY = desc(Entity.class); + + @TransformerTest("data.methods.inplace.SuperTypeParamUser") + void testSuperTypeParameter(final TransformerCheck check) { + final List methodNames = List.of("consume", "consumeStatic"); + final List rewrites = new ArrayList<>(); + for (final String name : methodNames) { + rewrites.add(new MethodRewrite(METHODS, exact(name), hasParameter(PLAYER), new SupertypeParam(ENTITY))); + } + + check.run(new RewriteProcessor(rewrites)); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapParamValue.java b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapParamValue.java new file mode 100644 index 0000000..754f5cc --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapParamValue.java @@ -0,0 +1,36 @@ +package io.papermc.classfile.method.action; + +import data.methods.Methods; +import data.methods.Redirects; +import data.types.hierarchy.loc.Location; +import data.types.hierarchy.loc.Position; +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TransformerTest; +import io.papermc.classfile.checks.TransformerCheck; +import io.papermc.classfile.method.MethodRewrite; +import java.lang.constant.ClassDesc; +import java.util.List; + +import static io.papermc.classfile.ClassFiles.desc; +import static io.papermc.classfile.method.MethodDescriptorPredicate.hasParameter; +import static io.papermc.classfile.method.MethodNamePredicate.constructor; +import static io.papermc.classfile.method.MethodNamePredicate.exact; + +class TestWrapParamValue { + + static final ClassDesc METHODS = desc(Methods.class); + static final ClassDesc METHODS_WRAPPER = desc(Methods.Wrapper.class); + static final ClassDesc REDIRECTS = desc(Redirects.class); + static final ClassDesc LOCATION = desc(Location.class); + static final ClassDesc POSITION = desc(Position.class); + + @TransformerTest("data.methods.statics.param.ParamDirectUser") + void testWrapParamValue(final TransformerCheck check) { + final WrapParamValue toPositionAction = new WrapParamValue(REDIRECTS, "toPosition", POSITION); + final List rewrites = List.of( + new MethodRewrite(METHODS, exact("consumeLoc", "consumeLocStatic"), hasParameter(LOCATION), toPositionAction), + new MethodRewrite(METHODS_WRAPPER, constructor(), hasParameter(LOCATION), toPositionAction) + ); + check.run(new RewriteProcessor(rewrites)); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapReturnValue.java b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapReturnValue.java new file mode 100644 index 0000000..84218ca --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapReturnValue.java @@ -0,0 +1,32 @@ +package io.papermc.classfile.method.action; + +import data.methods.Methods; +import data.methods.Redirects; +import data.types.hierarchy.loc.Location; +import data.types.hierarchy.loc.Position; +import io.papermc.classfile.RewriteProcessor; +import io.papermc.classfile.TransformerTest; +import io.papermc.classfile.checks.TransformerCheck; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.MethodRewrite; +import java.lang.constant.ClassDesc; +import java.util.List; + +import static io.papermc.classfile.ClassFiles.desc; + +class TestWrapReturnValue { + + static final ClassDesc METHODS = desc(Methods.class); + static final ClassDesc REDIRECTS = desc(Redirects.class); + static final ClassDesc LOCATION = desc(Location.class); + static final ClassDesc POSITION = desc(Position.class); + + @TransformerTest("data.methods.statics.returns.ReturnDirectUser") + void test(final TransformerCheck check) { + final List rewrites = List.of( + new MethodRewrite(METHODS, MethodNamePredicate.exact("getLoc", "getLocStatic"), MethodDescriptorPredicate.hasReturn(LOCATION), new WrapReturnValue(REDIRECTS, "wrapPosition", POSITION)) + ); + check.run(new RewriteProcessor(rewrites)); + } +} diff --git a/classfile-utils/src/test/java/io/papermc/classfile/method/transform/TestConstructorAwareCodeTransform.java b/classfile-utils/src/test/java/io/papermc/classfile/method/transform/TestConstructorAwareCodeTransform.java new file mode 100644 index 0000000..5ba4e70 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/transform/TestConstructorAwareCodeTransform.java @@ -0,0 +1,47 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.ClassFiles; +import java.lang.classfile.Opcode; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.classfile.constantpool.MethodRefEntry; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.NewObjectInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.lang.constant.MethodTypeDesc; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestConstructorAwareCodeTransform { + + @Test + void nonInvokeInstructionReturnsFalse() { + final NewObjectInstruction element = NewObjectInstruction.of(ConstantPoolBuilder.of().classEntry(ClassDesc.of("java.lang.Object"))); + assertThat(ConstructorAwareCodeTransform.isConstructor(element)).isFalse(); + } + + @Test + void invokespecialInitReturnsTrue() { + final ConstantPoolBuilder pool = ConstantPoolBuilder.of(); + final MethodRefEntry ref = pool.methodRefEntry(ClassDesc.of("java.lang.Object"), ClassFiles.CONSTRUCTOR_METHOD_NAME, MethodTypeDesc.of(ConstantDescs.CD_void)); + final InvokeInstruction invoke = InvokeInstruction.of(Opcode.INVOKESPECIAL, ref); + assertThat(ConstructorAwareCodeTransform.isConstructor(invoke)).isTrue(); + } + + @Test + void invokespecialNonInitReturnsFalse() { + final ConstantPoolBuilder pool = ConstantPoolBuilder.of(); + final MethodRefEntry ref = pool.methodRefEntry(ClassDesc.of("java.lang.Object"), "toString", MethodTypeDesc.of(ClassDesc.of("java.lang.String"))); + final InvokeInstruction invoke = InvokeInstruction.of(Opcode.INVOKESPECIAL, ref); + assertThat(ConstructorAwareCodeTransform.isConstructor(invoke)).isFalse(); + } + + @Test + void invokevirtualInitReturnsFalse() { + final ConstantPoolBuilder pool = ConstantPoolBuilder.of(); + final MethodRefEntry ref = pool.methodRefEntry(ClassDesc.of("java.lang.Object"), ClassFiles.CONSTRUCTOR_METHOD_NAME, MethodTypeDesc.of(ConstantDescs.CD_void)); + final InvokeInstruction invoke = InvokeInstruction.of(Opcode.INVOKEVIRTUAL, ref); + assertThat(ConstructorAwareCodeTransform.isConstructor(invoke)).isFalse(); + } +} diff --git a/classfile-utils/src/testData/java/data/classes/ClassToInterfaceRedirectUser.java b/classfile-utils/src/testData/java/data/classes/ClassToInterfaceRedirectUser.java new file mode 100644 index 0000000..b7d2de5 --- /dev/null +++ b/classfile-utils/src/testData/java/data/classes/ClassToInterfaceRedirectUser.java @@ -0,0 +1,34 @@ +package data.classes; + +import data.types.classes.SomeAbstractClass; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +public final class ClassToInterfaceRedirectUser extends SomeAbstractClass { + + public static void entry() { + final SomeAbstractClass someAbstractClass = new ClassToInterfaceRedirectUser(); + someAbstractClass.doSomething(); + final String name = someAbstractClass.getName(); + + final String staticString = SomeAbstractClass.getStaticString(); + + final Consumer doSomething = SomeAbstractClass::doSomething; + doSomething.accept(someAbstractClass); + + final Supplier getName = someAbstractClass::getName; + final String name2 = getName.get(); + + final Function getName2 = SomeAbstractClass::getName; + final String name3 = getName2.apply(someAbstractClass); + + final Supplier getStaticString = SomeAbstractClass::getStaticString; + final String staticString2 = getStaticString.get(); + } + + @Override + public void doSomething() { + } +} diff --git a/classfile-utils/src/testData/java/data/classes/ClassToInterfaceUser.java b/classfile-utils/src/testData/java/data/classes/ClassToInterfaceUser.java new file mode 100644 index 0000000..371430d --- /dev/null +++ b/classfile-utils/src/testData/java/data/classes/ClassToInterfaceUser.java @@ -0,0 +1,31 @@ +package data.classes; + +import data.types.classes.SomeAbstractClass; +import data.types.classes.SomeAbstractClassImpl; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +public final class ClassToInterfaceUser { + + public static void entry() { + final SomeAbstractClass someAbstractClass = new SomeAbstractClassImpl(); + someAbstractClass.doSomething(); + final String name = someAbstractClass.getName(); + + final String staticString = SomeAbstractClass.getStaticString(); + + final Consumer doSomething = SomeAbstractClass::doSomething; + doSomething.accept(someAbstractClass); + + final Supplier getName = someAbstractClass::getName; + final String name2 = getName.get(); + + final Function getName2 = SomeAbstractClass::getName; + final String name3 = getName2.apply(someAbstractClass); + + final Supplier getStaticString = SomeAbstractClass::getStaticString; + final String staticString2 = getStaticString.get(); + } +} diff --git a/classfile-utils/src/testData/java/data/classes/EnumToInterfaceUser.java b/classfile-utils/src/testData/java/data/classes/EnumToInterfaceUser.java new file mode 100644 index 0000000..ba9f584 --- /dev/null +++ b/classfile-utils/src/testData/java/data/classes/EnumToInterfaceUser.java @@ -0,0 +1,46 @@ +package data.classes; + +import data.types.apiimpl.ApiEnum; +import java.util.Arrays; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings({"unused", "UseOfSystemOutOrSystemErr", "UnnecessaryToStringCall"}) +public final class EnumToInterfaceUser { + + public static void entry() { + final ApiEnum a = ApiEnum.A; + // final String key = a.getKey(); + // System.out.println(key); + // + final Function getKey = ApiEnum::getKey; + final String lambdaKey = getKey.apply(a); + System.out.println(lambdaKey); + + final Supplier getKeySupplier = a::getKey; + final String keySupplier = getKeySupplier.get(); + System.out.println(keySupplier); + + final String keyStatic = ApiEnum.getKeyStatic(); + System.out.println(keyStatic); + + final Supplier getKeyStaticSupplier = ApiEnum::getKeyStatic; + final String keyStaticSupplier = getKeyStaticSupplier.get(); + System.out.println(keyStaticSupplier); + + System.out.println(a.compareTo(ApiEnum.B)); + System.out.println(ApiEnum.B.compareTo(a)); + + System.out.println(ApiEnum.C.name()); + // final Function name = ApiEnum::name; // cannot rewrite these because references to ApiEnum are lost in the bytecode + // System.out.println(name.apply(ApiEnum.C)); + // final Supplier nameSupplier = ApiEnum.C::name; + // System.out.println(nameSupplier.get()); + System.out.println(ApiEnum.C.ordinal()); + System.out.println(ApiEnum.A.toString()); + System.out.println(ApiEnum.A.getDeclaringClass()); + // + System.out.println(Arrays.toString(ApiEnum.values())); + System.out.println(ApiEnum.valueOf("A")); + } +} diff --git a/classfile-utils/src/testData/java/data/fields/FieldToMethodSameOwnerUser.java b/classfile-utils/src/testData/java/data/fields/FieldToMethodSameOwnerUser.java new file mode 100644 index 0000000..ab5d9f0 --- /dev/null +++ b/classfile-utils/src/testData/java/data/fields/FieldToMethodSameOwnerUser.java @@ -0,0 +1,16 @@ +package data.fields; + +import data.types.fields.FieldHolder; + +@SuppressWarnings({"unused", "StringOperationCanBeSimplified"}) +public final class FieldToMethodSameOwnerUser { + + public static void entry() { + final String s = FieldHolder.staticField; + FieldHolder.staticField = new String("other"); + + final FieldHolder holder = new FieldHolder(); + final String s2 = holder.instanceField; + holder.instanceField = new String("other"); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/Methods.java b/classfile-utils/src/testData/java/data/methods/Methods.java new file mode 100644 index 0000000..a7189a5 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/Methods.java @@ -0,0 +1,77 @@ +package data.methods; + +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +import data.types.hierarchy.loc.Location; +import data.types.hierarchy.loc.Position; + +@SuppressWarnings("unused") +public class Methods { + + public Entity get() { + return new Player(); + } + + public void consume(final Player player) { + player.getName(); + } + + public static Entity getStatic() { + return new Player(); + } + + public static void consumeStatic(final Player player) { + } + + public Location getLoc() { + return new Location(1, 2, 3); + } + + public static Location getLocStatic() { + return new Location(1, 2, 3); + } + + public boolean consumeLoc(final Location location) { + location.position(); + location.location(); + return true; + } + + public static boolean consumeLocStatic(final Location location) { + location.position(); + location.location(); + return true; + } + + public boolean consumePos(final Position position) { + position.position(); + return true; + } + + public static boolean consumePosStatic(final Position position) { + position.position(); + return true; + } + + public static class Wrapper { + + public Wrapper(Player player) { + } + + public Wrapper(Location location) { + } + + public Wrapper(Wrapper inner) { + } + + public Player getPlayer() { + return new Player(); + } + } + + public static class PosWrapper { + + public PosWrapper(Position position) { + } + } +} diff --git a/classfile-utils/src/testData/java/data/methods/inplace/SubTypeReturnUser.java b/classfile-utils/src/testData/java/data/methods/inplace/SubTypeReturnUser.java new file mode 100644 index 0000000..d8f8b4a --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/inplace/SubTypeReturnUser.java @@ -0,0 +1,24 @@ +package data.methods.inplace; + +import data.methods.Methods; +import data.types.hierarchy.Entity; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +public final class SubTypeReturnUser { + public static void entry() { + final Methods methods = new Methods(); + final Entity player = methods.get(); + final Entity player2 = Methods.getStatic(); + + final Supplier get = methods::get; + final Entity player3 = get.get(); + + final Function get2 = Methods::get; + final Entity player4 = get2.apply(methods); + + final Supplier getStatic = Methods::getStatic; + final Entity player5 = getStatic.get(); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/inplace/SuperTypeParamUser.java b/classfile-utils/src/testData/java/data/methods/inplace/SuperTypeParamUser.java new file mode 100644 index 0000000..1d9faaa --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/inplace/SuperTypeParamUser.java @@ -0,0 +1,25 @@ +package data.methods.inplace; + +import data.methods.Methods; +import data.types.hierarchy.Player; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@SuppressWarnings("unused") +public final class SuperTypeParamUser { + + public static void entry() { + final Methods methods = new Methods(); + methods.consume(new Player()); + Methods.consumeStatic(new Player()); + + final Consumer consume = methods::consume; + consume.accept(new Player()); + + final BiConsumer consume2 = Methods::consume; + consume2.accept(methods, new Player()); + + final Consumer consumeStatic = Methods::consumeStatic; + consumeStatic.accept(new Player()); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/statics/MoveToInstanceUser.java b/classfile-utils/src/testData/java/data/methods/statics/MoveToInstanceUser.java new file mode 100644 index 0000000..ce9e13f --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/MoveToInstanceUser.java @@ -0,0 +1,28 @@ +package data.methods.statics; + +import data.types.apiimpl.ApiInterface; +import data.types.apiimpl.ApiInterfaceImpl; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +public final class MoveToInstanceUser { + + public static void entry() { + final ApiInterface apiInterface = get(); + final String s = apiInterface.get(); + System.out.println(s); + + final Supplier get = apiInterface::get; + final String s2 = get.get(); + System.out.println(s2); + + final Function get2 = ApiInterface::get; + final String s3 = get2.apply(apiInterface); + System.out.println(s3); + } + + private static ApiInterface get() { + return new ApiInterfaceImpl(); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java b/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java new file mode 100644 index 0000000..3ce3cc5 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java @@ -0,0 +1,62 @@ +package data.methods.statics; + +import data.methods.Methods; +import data.types.hierarchy.Entity; +import data.types.hierarchy.Mob; +import data.types.hierarchy.Player; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +@SuppressWarnings("unused") +final class PlainUser { + + public static void entry() { + final Player player = new Player(); + final Entity entity = new Mob(); + final Entity entity2 = new Mob(); + player.addEntity(entity); + Player.addEntityStatic(player); + + new Methods.Wrapper(player); + new StringBuilder(new Methods.Wrapper(new Player()).toString()); + + new Methods.Wrapper(new Methods.Wrapper((Methods.Wrapper) null).getPlayer()); + + final BiConsumer addEntity = Player::addEntity; + addEntity.accept(player, entity); + + final Consumer addEntity2 = player::addEntity; + addEntity2.accept(entity); + + final BiConsumer addEntityAndPlayer = player::addEntityAndPlayer; + addEntityAndPlayer.accept(player, entity); + + final Consumer addEntityStatic = Player::addEntityStatic; + addEntityStatic.accept(entity); + + final BiConsumer addEntityAndPlayerStatic = Player::addEntityAndPlayerStatic; + addEntityAndPlayerStatic.accept(player, entity); + + final Function wrapper = Methods.Wrapper::new; + wrapper.apply(player); + + entity.setOwner(player); + entity.setOwner(entity2); + + final Consumer setPlayerOwner = entity::setOwner; + setPlayerOwner.accept(player); + + final Consumer setEntityOwner = entity::setOwner; + setEntityOwner.accept(entity2); + + final BiConsumer setEntityOwner2 = Entity::setOwner; + setEntityOwner2.accept(entity, entity2); + + final BiConsumer setPlayerOwner2 = Entity::setOwner; + setPlayerOwner2.accept(entity, player); + + final BiConsumer setOwner = Entity::setOwner; + setOwner.accept(entity, entity2); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/statics/param/ParamDirectUser.java b/classfile-utils/src/testData/java/data/methods/statics/param/ParamDirectUser.java new file mode 100644 index 0000000..e1464e3 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/param/ParamDirectUser.java @@ -0,0 +1,32 @@ +package data.methods.statics.param; + +import data.methods.Methods; +import data.types.hierarchy.loc.Location; +import java.util.function.BiFunction; +import java.util.function.Function; + +@SuppressWarnings("unused") +final class ParamDirectUser { + + public static void entry() { + final Location loc = new Location(1, 2, 3); + final boolean b = Methods.consumeLocStatic(loc); + + final Methods methods = new Methods(); + final boolean b1 = methods.consumeLoc(loc); + + final Methods.Wrapper wrapper = new Methods.Wrapper(loc); + + final Function consumeLocStatic = Methods::consumeLocStatic; + final Boolean b2 = consumeLocStatic.apply(loc); + + final BiFunction consumeLoc = Methods::consumeLoc; + final Boolean b3 = consumeLoc.apply(methods, loc); + + final Function consumeLoc2 = methods::consumeLoc; + final Boolean b4 = consumeLoc2.apply(loc); + + final Function newWrapper = Methods.Wrapper::new; + newWrapper.apply(loc); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/statics/param/ParamFuzzyUser.java b/classfile-utils/src/testData/java/data/methods/statics/param/ParamFuzzyUser.java new file mode 100644 index 0000000..6bf8188 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/param/ParamFuzzyUser.java @@ -0,0 +1,42 @@ +package data.methods.statics.param; + +import data.methods.Methods; +import data.types.hierarchy.loc.Location; +import data.types.hierarchy.loc.Position; +import data.types.hierarchy.loc.PositionImpl; +import java.util.function.BiFunction; +import java.util.function.Function; + +@SuppressWarnings("unused") +public final class ParamFuzzyUser { + public static void entry() { + final Location loc = new Location(1, 2, 3); + final boolean b = Methods.consumePosStatic(loc); + + final Position pos = new PositionImpl(1, 2, 3); + final boolean bb = Methods.consumePosStatic(pos); + + final Methods methods = new Methods(); + final boolean b1 = methods.consumePos(loc); + final boolean bb1 = methods.consumePos(pos); + + new Methods.PosWrapper(loc); + new Methods.PosWrapper(pos); + + final Function consumeLocStatic = Methods::consumePosStatic; + final Boolean b2 = consumeLocStatic.apply(loc); + final Boolean bb2 = consumeLocStatic.apply(pos); + + final BiFunction consumeLoc = Methods::consumePos; + final Boolean b3 = consumeLoc.apply(methods, loc); + final boolean bb3 = consumeLoc.apply(methods, pos); + + final Function consumeLoc2 = methods::consumePos; + final Boolean b4 = consumeLoc2.apply(loc); + final Boolean bb4 = consumeLoc2.apply(pos); + + final Function newWrapper = Methods.PosWrapper::new; + newWrapper.apply(loc); + newWrapper.apply(pos); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectUser.java b/classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectUser.java new file mode 100644 index 0000000..b6fea65 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectUser.java @@ -0,0 +1,34 @@ +package data.methods.statics.returns; + +import data.methods.Methods; +import data.types.hierarchy.loc.Location; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +final class ReturnDirectUser { + + public static void entry() { + final Location loc = Methods.getLocStatic(); + loc.position(); + loc.location(); + System.out.println(loc); + + final Methods methods = new Methods(); + final Location loc1 = methods.getLoc(); + loc1.position(); + loc1.location(); + System.out.println(loc1); + + final Supplier getLocStatic = Methods::getLocStatic; + final Location loc2 = getLocStatic.get(); + loc2.position(); + loc2.location(); + System.out.println(loc2); + + final Supplier getLoc = methods::getLoc; + final Location loc3 = getLoc.get(); + loc3.position(); + loc3.location(); + System.out.println(loc3); + } +} diff --git a/classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectWithContextUser.java b/classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectWithContextUser.java new file mode 100644 index 0000000..07eb579 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectWithContextUser.java @@ -0,0 +1,34 @@ +package data.methods.statics.returns; + +import data.methods.Methods; +import data.types.hierarchy.loc.Location; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +final class ReturnDirectWithContextUser { + + public static void entry() { + final Location loc = Methods.getLocStatic(); + loc.position(); + loc.location(); + System.out.println(loc); + + final Methods methods = new Methods(); + final Location loc1 = methods.getLoc(); + loc1.position(); + loc1.location(); + System.out.println(loc1); + + final Supplier getLocStatic = Methods::getLocStatic; + final Location loc2 = getLocStatic.get(); + loc2.position(); + loc2.location(); + System.out.println(loc2); + + final Supplier getLoc = methods::getLoc; + final Location loc3 = getLoc.get(); + loc3.position(); + loc3.location(); + System.out.println(loc3); + } +} diff --git a/classfile-utils/src/testData/java/data/rename/RenameTest.java b/classfile-utils/src/testData/java/data/rename/RenameTest.java new file mode 100644 index 0000000..1232421 --- /dev/null +++ b/classfile-utils/src/testData/java/data/rename/RenameTest.java @@ -0,0 +1,42 @@ +package data.rename; + +import data.types.rename.TestAnnotation; +import data.types.rename.TestEnum; +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; + +@SuppressWarnings("unused") +@TestAnnotation(single = TestEnum.A, multiple = {TestEnum.A, TestEnum.B, TestEnum.C}, clazz = TestEnum.class) +public final class RenameTest { + + @TestAnnotation(single = TestEnum.A, multiple = {TestEnum.A, TestEnum.B, TestEnum.C}, clazz = TestEnum.class) + public static void entry() throws ReflectiveOperationException { + checkAnnotation(RenameTest.class); + checkAnnotation(RenameTest.class.getDeclaredMethod("entry")); + checkAnnotation(RenameTest.class.getDeclaredField("field")); + checkAnnotation(RenameTest.class.getDeclaredField("otherField")); + + final TestEnum a = TestEnum.valueOf("A"); + System.out.println(a); + final TestEnum fb = TestEnum.valueOf("FB"); + System.out.println(fb); + final TestEnum ea = TestEnum.valueOf("Ea"); + System.out.println(ea); + + a.method1(1); + fb.method2(2); + } + + private static void checkAnnotation(final AnnotatedElement element) { + final TestAnnotation annotation = element.getAnnotation(TestAnnotation.class); + System.out.println(annotation.single()); + System.out.println(Arrays.toString(annotation.multiple())); + System.out.println(annotation.clazz()); + } + + @TestAnnotation(single = TestEnum.A, clazz = TestEnum.class) + public static final String field = ""; + + @TestAnnotation(single = TestEnum.A, multiple = {TestEnum.A, TestEnum.B, TestEnum.C}) + public static final String otherField = ""; +} diff --git a/classfile-utils/src/testData/java/data/types/apiimpl/ApiEnum.java b/classfile-utils/src/testData/java/data/types/apiimpl/ApiEnum.java new file mode 100644 index 0000000..65201d7 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/apiimpl/ApiEnum.java @@ -0,0 +1,21 @@ +package data.types.apiimpl; + +public enum ApiEnum { + A("A"), + B("B"), + C("C"); + + private final String key; + + ApiEnum(final String key) { + this.key = key; + } + + public static String getKeyStatic() { + return "testStatic"; + } + + public String getKey() { + return this.key; + } +} diff --git a/classfile-utils/src/testData/java/data/types/apiimpl/ApiInterface.java b/classfile-utils/src/testData/java/data/types/apiimpl/ApiInterface.java new file mode 100644 index 0000000..9c9ffee --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/apiimpl/ApiInterface.java @@ -0,0 +1,6 @@ +package data.types.apiimpl; + +public interface ApiInterface { + + String get(); +} diff --git a/classfile-utils/src/testData/java/data/types/apiimpl/ApiInterfaceImpl.java b/classfile-utils/src/testData/java/data/types/apiimpl/ApiInterfaceImpl.java new file mode 100644 index 0000000..5c1ed68 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/apiimpl/ApiInterfaceImpl.java @@ -0,0 +1,9 @@ +package data.types.apiimpl; + +public class ApiInterfaceImpl implements ApiInterface { + + @Override + public String get() { + return ""; + } +} diff --git a/classfile-utils/src/testData/java/data/types/classes/SomeAbstractClass.java b/classfile-utils/src/testData/java/data/types/classes/SomeAbstractClass.java new file mode 100644 index 0000000..e697670 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/classes/SomeAbstractClass.java @@ -0,0 +1,14 @@ +package data.types.classes; + +public abstract class SomeAbstractClass { + + public static String getStaticString() { + return "test"; + } + + public String getName() { + return "test"; + } + + public abstract void doSomething(); +} diff --git a/classfile-utils/src/testData/java/data/types/classes/SomeAbstractClassImpl.java b/classfile-utils/src/testData/java/data/types/classes/SomeAbstractClassImpl.java new file mode 100644 index 0000000..a879655 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/classes/SomeAbstractClassImpl.java @@ -0,0 +1,8 @@ +package data.types.classes; + +public class SomeAbstractClassImpl extends SomeAbstractClass { + + @Override + public void doSomething() { + } +} diff --git a/classfile-utils/src/testData/java/data/types/fields/FieldHolder.java b/classfile-utils/src/testData/java/data/types/fields/FieldHolder.java new file mode 100644 index 0000000..f114911 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/fields/FieldHolder.java @@ -0,0 +1,8 @@ +package data.types.fields; + +public class FieldHolder { + + public static String staticField = ""; + + public String instanceField = ""; +} diff --git a/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java b/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java new file mode 100644 index 0000000..d83d4ba --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java @@ -0,0 +1,14 @@ +package data.types.hierarchy; + +import org.jspecify.annotations.Nullable; + +public interface Entity { + + String getName(); + + void setOwner(@Nullable Entity entity); + + void setOwner(@Nullable Player player); + + @Nullable Entity getOwner(); +} diff --git a/classfile-utils/src/testData/java/data/types/hierarchy/Mob.java b/classfile-utils/src/testData/java/data/types/hierarchy/Mob.java new file mode 100644 index 0000000..ca76214 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Mob.java @@ -0,0 +1,28 @@ +package data.types.hierarchy; + +import org.jspecify.annotations.Nullable; + +public class Mob implements Entity { + + private @Nullable Entity owner = null; + + @Override + public String getName() { + return "MOB"; + } + + @Override + public void setOwner(final @Nullable Entity entity) { + this.owner = entity; + } + + @Override + public void setOwner(final @Nullable Player player) { + this.owner = player; + } + + @Override + public @Nullable Entity getOwner() { + return this.owner; + } +} diff --git a/classfile-utils/src/testData/java/data/types/hierarchy/Player.java b/classfile-utils/src/testData/java/data/types/hierarchy/Player.java new file mode 100644 index 0000000..ccff3f8 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Player.java @@ -0,0 +1,50 @@ +package data.types.hierarchy; + +import org.jspecify.annotations.Nullable; + +@SuppressWarnings("unused") +public class Player implements Entity { + + private @Nullable Entity owner = null; + + @Override + public String getName() { + return "Player"; + } + + public void addEntity(final Entity entity) { + entity.getName(); + } + + public void addEntityAndPlayer(final Player player, final Entity entity) { + entity.getName(); + player.getName(); + } + + public static void addEntityStatic(final Entity entity) { + entity.getName(); + } + + public static void addEntityAndPlayerStatic(final Player player, final Entity entity) { + player.getName(); + entity.getName(); + } + + void test() { + } + + @Override + public void setOwner(final @Nullable Entity entity) { + this.owner = entity; + } + + @Override + public void setOwner(final @Nullable Player player) { + this.owner = player; + } + + @Override + public @Nullable Entity getOwner() { + return this.owner; + } +} diff --git a/classfile-utils/src/testData/java/data/types/hierarchy/loc/Location.java b/classfile-utils/src/testData/java/data/types/hierarchy/loc/Location.java new file mode 100644 index 0000000..7abf97f --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/loc/Location.java @@ -0,0 +1,36 @@ +package data.types.hierarchy.loc; + +public class Location implements Position { + + private int x; + private int y; + private int z; + + public Location(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public int x() { + return this.x; + } + + @Override + public int y() { + return this.y; + } + + @Override + public int z() { + return this.z; + } + + @Override + public void position() { + } + + public void location() { + } +} diff --git a/classfile-utils/src/testData/java/data/types/hierarchy/loc/Position.java b/classfile-utils/src/testData/java/data/types/hierarchy/loc/Position.java new file mode 100644 index 0000000..f471db3 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/loc/Position.java @@ -0,0 +1,12 @@ +package data.types.hierarchy.loc; + +public interface Position { + + int x(); + + int y(); + + int z(); + + void position(); +} diff --git a/classfile-utils/src/testData/java/data/types/hierarchy/loc/PositionImpl.java b/classfile-utils/src/testData/java/data/types/hierarchy/loc/PositionImpl.java new file mode 100644 index 0000000..dfcc3f1 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/loc/PositionImpl.java @@ -0,0 +1,8 @@ +package data.types.hierarchy.loc; + +public record PositionImpl(int x, int y, int z) implements Position { + + @Override + public void position() { + } +} diff --git a/classfile-utils/src/testData/java/data/types/rename/TestAnnotation.java b/classfile-utils/src/testData/java/data/types/rename/TestAnnotation.java new file mode 100644 index 0000000..9cf4e1a --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/rename/TestAnnotation.java @@ -0,0 +1,14 @@ +package data.types.rename; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface TestAnnotation { + + TestEnum single(); + + TestEnum[] multiple() default {}; + + Class clazz() default void.class; +} diff --git a/classfile-utils/src/testData/java/data/types/rename/TestEnum.java b/classfile-utils/src/testData/java/data/types/rename/TestEnum.java new file mode 100644 index 0000000..9714c3f --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/rename/TestEnum.java @@ -0,0 +1,16 @@ +package data.types.rename; + +public enum TestEnum { + A, + B, + C, + FB, // FB and Ea have the same string hashCode + Ea, + ; + + public void method1(final int param1) { + } + + public void method2(final int param1) { + } +} diff --git a/classfile-utils/src/testData/resources/expected/data/methods/inplace/SubTypeReturnUser.class b/classfile-utils/src/testData/resources/expected/data/methods/inplace/SubTypeReturnUser.class new file mode 100644 index 0000000..9cdea63 Binary files /dev/null and b/classfile-utils/src/testData/resources/expected/data/methods/inplace/SubTypeReturnUser.class differ diff --git a/classfile-utils/src/testData/resources/expected/data/methods/inplace/SuperTypeParamUser.class b/classfile-utils/src/testData/resources/expected/data/methods/inplace/SuperTypeParamUser.class new file mode 100644 index 0000000..c0fe4d9 Binary files /dev/null and b/classfile-utils/src/testData/resources/expected/data/methods/inplace/SuperTypeParamUser.class differ diff --git a/classfile-utils/src/testData/resources/expected/data/methods/statics/PlainUser.class b/classfile-utils/src/testData/resources/expected/data/methods/statics/PlainUser.class new file mode 100644 index 0000000..2c038c7 Binary files /dev/null and b/classfile-utils/src/testData/resources/expected/data/methods/statics/PlainUser.class differ diff --git a/classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamDirectUser.class b/classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamDirectUser.class new file mode 100644 index 0000000..36a3bb9 Binary files /dev/null and b/classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamDirectUser.class differ diff --git a/classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamFuzzyUser.class b/classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamFuzzyUser.class new file mode 100644 index 0000000..9b38a69 Binary files /dev/null and b/classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamFuzzyUser.class differ diff --git a/classfile-utils/src/testData/resources/expected/data/methods/statics/returns/ReturnDirectUser.class b/classfile-utils/src/testData/resources/expected/data/methods/statics/returns/ReturnDirectUser.class new file mode 100644 index 0000000..27e7793 Binary files /dev/null and b/classfile-utils/src/testData/resources/expected/data/methods/statics/returns/ReturnDirectUser.class differ diff --git a/classfile-utils/src/testDataNewTargets/java/data/SameClassTarget.java b/classfile-utils/src/testDataNewTargets/java/data/SameClassTarget.java new file mode 100644 index 0000000..849e683 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/SameClassTarget.java @@ -0,0 +1,7 @@ +package data; + +public class SameClassTarget { + public static final InnerCls B = new InnerCls("B"); + + private record InnerCls(String s) {} +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/methods/Methods.java b/classfile-utils/src/testDataNewTargets/java/data/methods/Methods.java new file mode 100644 index 0000000..b7154ec --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/methods/Methods.java @@ -0,0 +1,87 @@ +package data.methods; + +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +import data.types.hierarchy.loc.Location; +import data.types.hierarchy.loc.Position; +import data.types.hierarchy.loc.PositionImpl; + +@SuppressWarnings("unused") +public class Methods { + + public Player get() { + return new Player(); + } + + public void consume(final Entity entity) { + entity.getName(); + } + + public static Player getStatic() { + return new Player(); + } + + public static void consumeStatic(final Entity entity) { + } + + public Position getLoc() { + return new PositionImpl(1, 2, 3); + } + + public static Position getLocStatic() { + return new PositionImpl(1, 2, 3); + } + + public boolean consumeLoc(final Position pos) { + pos.position(); + System.out.println(pos.getClass()); + return true; + } + + public static boolean consumeLocStatic(final Position pos) { + pos.position(); + System.out.println(pos.getClass()); + return true; + } + + public boolean consumePos(final Position position) { + position.position(); + System.out.println(position.getClass()); + return true; + } + + public static boolean consumePosStatic(final Position position) { + position.position(); + System.out.println(position.getClass()); + return true; + } + + public static class Wrapper { + + public Wrapper(Entity player) { + System.out.println(player.getClass()); + System.out.println(player.getName()); + } + + public Wrapper(Position position) { + position.position(); + System.out.println(position.getClass()); + } + + public Wrapper(Wrapper inner) { + System.out.println(inner); + } + + public Player getPlayer() { + return new Player(); + } + } + + public static class PosWrapper { + + public PosWrapper(Position position) { + position.position(); + System.out.println(position.getClass()); + } + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java b/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java new file mode 100644 index 0000000..9962981 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java @@ -0,0 +1,62 @@ +package data.methods; + +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +import data.types.hierarchy.loc.Location; +import data.types.hierarchy.loc.Position; +import data.types.hierarchy.loc.PositionImpl; + +@SuppressWarnings("unused") +public final class Redirects { + + public static void addEntity(final Player player, final Entity entity) { + player.addEntity(entity); + } + + public static void addEntityAndPlayer(final Player root, final Player player, final Entity entity) { + root.addEntityAndPlayer(player, entity); + } + + public static void addEntityStatic(final Entity entity) { + entity.getName(); + } + + public static void addEntityAndPlayerStatic(final Player player, final Entity entity) { + Player.addEntityAndPlayerStatic(player, entity); + } + + public static Methods.Wrapper createMethods$Wrapper(final Player entity) { + return new Methods.Wrapper(entity); + } + + public static Position toPosition(final Location location) { + return new PositionImpl(location.x(), location.y(), location.z()); + } + + public static Location wrapPosition(final Position input) { + return new Location(input.x(), input.y(), input.z(), "wrapped"); + } + + public static Location wrapPositionWithContext(final Methods methods, final Position input) { + return new Location(input.x(), input.y(), input.z(), "ctx=" + methods + " wrapped"); + } + + public static Position toPositionFuzzy(final Object maybeLocation) { + if (maybeLocation instanceof final Position pos) { + System.out.println("was pos"); + return pos; + } + System.out.println("was not pos"); + return toPosition((Location) maybeLocation); + } + + public static void wrapObject(final Object object) { + } + + public static void setOwner(final Entity root, final Entity target) { + root.setOwnerNew(target); + } + + private Redirects() { + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnum.java b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnum.java new file mode 100644 index 0000000..520a601 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnum.java @@ -0,0 +1,14 @@ +package data.types.apiimpl; + +public interface ApiEnum { + + static String getKeyStatic() { + return "testStatic"; + } + + ApiEnum A = new ApiEnumImpl("A"); + ApiEnum B = new ApiEnumImpl("B"); + ApiEnum C = new ApiEnumImpl("C"); + + String getKey(); +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnumImpl.java b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnumImpl.java new file mode 100644 index 0000000..48671a0 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnumImpl.java @@ -0,0 +1,58 @@ +package data.types.apiimpl; + +import io.papermc.asm.rules.classes.LegacyEnum; + +public final class ApiEnumImpl implements ApiEnum, LegacyEnum { + + private static int count = 0; + + private final String key; + private final int ordinal; + + ApiEnumImpl(final String key) { + this.key = key; + this.ordinal = count++; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String name() { + return this.key; + } + + @Override + public int ordinal() { + return this.ordinal; + } + + @Override + public int compareTo(final ApiEnumImpl o) { + return this.ordinal - o.ordinal; + } + + @Override + public String toString() { + return this.name(); + } + + public static ApiEnum[] values() { + final ApiEnum[] values = new ApiEnum[3]; + values[0] = ApiEnum.A; + values[1] = ApiEnum.B; + values[2] = ApiEnum.C; + return values; + } + + public static ApiEnum valueOf(final String name) { + return switch (name) { + case "A" -> ApiEnum.A; + case "B" -> ApiEnum.B; + case "C" -> ApiEnum.C; + default -> throw new IllegalArgumentException("No value exists for name " + name); + }; + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterface.java b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterface.java new file mode 100644 index 0000000..327fba6 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterface.java @@ -0,0 +1,6 @@ +package data.types.apiimpl; + +public interface ApiInterface { + + int get(); +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterfaceImpl.java b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterfaceImpl.java new file mode 100644 index 0000000..51328fe --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterfaceImpl.java @@ -0,0 +1,14 @@ +package data.types.apiimpl; + +public class ApiInterfaceImpl implements ApiInterface { + + @Override + public int get() { + return 0; + } + + // leave method because bytecode rewriting uses it + public String get0() { + return "rewritten"; + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/classes/AbstractSomeAbstractClass.java b/classfile-utils/src/testDataNewTargets/java/data/types/classes/AbstractSomeAbstractClass.java new file mode 100644 index 0000000..392d008 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/classes/AbstractSomeAbstractClass.java @@ -0,0 +1,9 @@ +package data.types.classes; + +public abstract class AbstractSomeAbstractClass implements SomeAbstractClass { + + @Override + public String getName() { + return ""; + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClass.java b/classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClass.java new file mode 100644 index 0000000..ce2217c --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClass.java @@ -0,0 +1,12 @@ +package data.types.classes; + +public interface SomeAbstractClass { + + static String getStaticString() { + return "test"; + } + + String getName(); + + void doSomething(); +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClassImpl.java b/classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClassImpl.java new file mode 100644 index 0000000..1e0fbc2 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClassImpl.java @@ -0,0 +1,8 @@ +package data.types.classes; + +public class SomeAbstractClassImpl extends AbstractSomeAbstractClass { + + @Override + public void doSomething() { + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/fields/FieldHolder.java b/classfile-utils/src/testDataNewTargets/java/data/types/fields/FieldHolder.java new file mode 100644 index 0000000..e0dfcae --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/fields/FieldHolder.java @@ -0,0 +1,24 @@ +package data.types.fields; + +@SuppressWarnings("unused") +public class FieldHolder { + + private static String staticField = ""; + private String instanceField = ""; + + public static String getStaticField() { + return staticField; + } + + public static void setStaticField(final String value) { + staticField = value; + } + + public String getInstanceField() { + return this.instanceField; + } + + public void setInstanceField(final String value) { + this.instanceField = value; + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java new file mode 100644 index 0000000..70656c4 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java @@ -0,0 +1,14 @@ +package data.types.hierarchy; + +import org.jspecify.annotations.Nullable; + +public interface Entity { + + String getName(); + + void setOwnerNew(@Nullable Entity entity); + + void setOwner(@Nullable Player player); + + @Nullable Entity getOwner(); +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Mob.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Mob.java new file mode 100644 index 0000000..1862d93 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Mob.java @@ -0,0 +1,37 @@ +package data.types.hierarchy; + +import org.jspecify.annotations.Nullable; + +@SuppressWarnings("ConstantValue") +public class Mob implements Entity { + + private @Nullable Entity owner = null; + + @Override + public String getName() { + return "MOB"; + } + + @Override + public void setOwnerNew(final @Nullable Entity entity) { + System.out.println("Set entity owner to " + entity + " on Mob"); + this.owner = entity; + if (this.owner != null) { + assert this.owner instanceof Entity; + } + } + + @Override + public void setOwner(final @Nullable Player player) { + System.out.println("Set player owner to " + player + " on Mob"); + this.owner = player; + if (this.owner != null) { + assert this.owner instanceof Player; + } + } + + @Override + public @Nullable Entity getOwner() { + return this.owner; + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java new file mode 100644 index 0000000..302a18e --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java @@ -0,0 +1,70 @@ +package data.types.hierarchy; + +import org.jspecify.annotations.Nullable; + +@SuppressWarnings({"unused", "DataFlowIssue"}) +public class Player implements Entity { + + public Player() { + } + + String data = ""; + public Player(final String data) { + this.data = data; + } + + @Override + public String getName() { + return "Player"; + } + + public void addEntity(final Entity entity) { + entity.getName(); + } + + public void addEntityAndPlayer(final Player player, final Entity entity) { + entity.getName(); + player.getName(); + } + + public static void addEntityStatic(final Entity entity) { + entity.getName(); + } + + public static void addEntityAndPlayerStatic(final Player player, final Entity entity) { + player.getName(); + entity.getName(); + } + + @Override + public String toString() { + return this.data + super.toString(); + } + + private @Nullable Entity owner; + + @Override + public void setOwnerNew(final @Nullable Entity entity) { + System.out.println("Set owner to " + entity + " on Player"); + this.owner = entity; + if (this.owner != null) { + this.owner.getName(); + assert this.owner instanceof Entity; + } + } + + @Override + public void setOwner(final @Nullable Player player) { + System.out.println("Set player owner to " + player + " on Player"); + this.owner = player; + if (this.owner != null) { + this.owner.getName(); + assert this.owner instanceof Player; + } + } + + @Override + public @Nullable Entity getOwner() { + return this.owner; + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Location.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Location.java new file mode 100644 index 0000000..b72532d --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Location.java @@ -0,0 +1,41 @@ +package data.types.hierarchy.loc; + +public class Location { + + private int x; + private int y; + private int z; + private String data; + + public Location(final int x, final int y, final int z) { + } + public Location(final int x, final int y, final int z, final String data) { + this.x = x; + this.y = y; + this.z = z; + this.data = data; + } + + public int x() { + return this.x; + } + + public int y() { + return this.y; + } + + public int z() { + return this.z; + } + + public void position() { + } + + public void location() { + } + + @Override + public String toString() { + return this.data + super.toString(); + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Position.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Position.java new file mode 100644 index 0000000..f471db3 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Position.java @@ -0,0 +1,12 @@ +package data.types.hierarchy.loc; + +public interface Position { + + int x(); + + int y(); + + int z(); + + void position(); +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/PositionImpl.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/PositionImpl.java new file mode 100644 index 0000000..dfcc3f1 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/PositionImpl.java @@ -0,0 +1,8 @@ +package data.types.hierarchy.loc; + +public record PositionImpl(int x, int y, int z) implements Position { + + @Override + public void position() { + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java b/classfile-utils/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java new file mode 100644 index 0000000..1ae2e8d --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java @@ -0,0 +1,16 @@ +package data.types.rename; + +public enum RenamedTestEnum { + ONE, + TWO, + THREE, + FOUR, + FIVE, + ; + + public void renamed_method1(final int param1) { + } + + public void renamed_method2(final int param1) { + } +} diff --git a/classfile-utils/src/testDataNewTargets/java/data/types/rename/TestAnnotation.java b/classfile-utils/src/testDataNewTargets/java/data/types/rename/TestAnnotation.java new file mode 100644 index 0000000..5b92b2d --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/rename/TestAnnotation.java @@ -0,0 +1,14 @@ +package data.types.rename; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface TestAnnotation { + + RenamedTestEnum value(); + + RenamedTestEnum[] multiple() default {}; + + Class clazz() default void.class; +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7aeeb2..a25debb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,8 @@ [versions] asm = "9.9.1" junit = "6.0.3" +assertj = "3.27.7" +mockito = "5.22.0" indra = "3.2.0" jbAnnos = "26.1.0" @@ -15,6 +17,9 @@ jupiterApi = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "ju jupiterEngine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } jupiterParams = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } platformLauncher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" } +assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } gradle-kotlin-dsl = "org.gradle.kotlin.kotlin-dsl:org.gradle.kotlin.kotlin-dsl.gradle.plugin:6.4.2" gradle-plugin-kotlin = { module = "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1ee188a..d72bdaa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,6 @@ project(":reflection-rewriter/proxy-generator").name = "reflection-rewriter-prox include("reflection-rewriter/runtime") project(":reflection-rewriter/runtime").name = "reflection-rewriter-runtime" + +include("classfile-utils") +project(":classfile-utils").name = "classfile-utils"