From ba20ab0451875e7d70b41b0147e148d06b8186a9 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sun, 1 Mar 2026 00:16:15 -0800 Subject: [PATCH 1/4] Start migrating to Java 25 ClassFile API --- build-logic/build.gradle.kts | 4 + .../src/main/kotlin/config-java25.gradle.kts | 66 ++++++ classfile-utils/build.gradle.kts | 86 +++++++ .../io/papermc/classfile/ClassfileUtils.java | 26 +++ .../java/io/papermc/classfile/MethodType.java | 6 + .../papermc/classfile/RewriteProcessor.java | 61 +++++ .../method/MethodDescriptorPredicate.java | 34 +++ .../classfile/method/MethodNamePredicate.java | 65 ++++++ .../classfile/method/MethodRewrite.java | 161 +++++++++++++ .../method/action/DirectStaticCall.java | 98 ++++++++ .../method/action/MethodRewriteAction.java | 32 +++ .../io/papermc/classfile/package-info.java | 4 + .../papermc/asm/rules/classes/LegacyEnum.java | 30 +++ .../java/io/papermc/classfile/SimpleTest.java | 38 +++ .../java/io/papermc/classfile/TestUtil.java | 216 ++++++++++++++++++ .../io/papermc/classfile/TransformerTest.java | 21 ++ .../checks/ExecutionTransformerCheck.java | 12 + .../checks/RewriteTransformerCheck.java | 12 + .../classfile/checks/TransformerCheck.java | 8 + .../checks/TransformerChecksProvider.java | 21 ++ .../classfile/checks/package-info.java | 4 + .../classes/ClassToInterfaceRedirectUser.java | 34 +++ .../data/classes/ClassToInterfaceUser.java | 31 +++ .../data/classes/EnumToInterfaceUser.java | 46 ++++ .../fields/FieldToMethodSameOwnerUser.java | 16 ++ .../testData/java/data/methods/Methods.java | 77 +++++++ .../methods/inplace/SubTypeReturnUser.java | 24 ++ .../methods/inplace/SuperTypeParamUser.java | 25 ++ .../methods/statics/MoveToInstanceUser.java | 28 +++ .../java/data/methods/statics/PlainUser.java | 42 ++++ .../statics/param/ParamDirectUser.java | 32 +++ .../methods/statics/param/ParamFuzzyUser.java | 42 ++++ .../statics/returns/ReturnDirectUser.java | 34 +++ .../returns/ReturnDirectWithContextUser.java | 34 +++ .../testData/java/data/rename/RenameTest.java | 42 ++++ .../java/data/types/apiimpl/ApiEnum.java | 21 ++ .../java/data/types/apiimpl/ApiInterface.java | 6 + .../data/types/apiimpl/ApiInterfaceImpl.java | 9 + .../data/types/classes/SomeAbstractClass.java | 14 ++ .../types/classes/SomeAbstractClassImpl.java | 8 + .../java/data/types/fields/FieldHolder.java | 8 + .../java/data/types/hierarchy/Entity.java | 6 + .../java/data/types/hierarchy/Mob.java | 9 + .../java/data/types/hierarchy/Player.java | 31 +++ .../data/types/hierarchy/loc/Location.java | 36 +++ .../data/types/hierarchy/loc/Position.java | 12 + .../types/hierarchy/loc/PositionImpl.java | 8 + .../data/types/rename/TestAnnotation.java | 14 ++ .../java/data/types/rename/TestEnum.java | 16 ++ .../data/methods/statics/PlainUser.class | Bin 0 -> 3279 bytes .../java/data/SameClassTarget.java | 7 + .../java/data/methods/Methods.java | 87 +++++++ .../java/data/methods/Redirects.java | 58 +++++ .../java/data/types/apiimpl/ApiEnum.java | 14 ++ .../java/data/types/apiimpl/ApiEnumImpl.java | 58 +++++ .../java/data/types/apiimpl/ApiInterface.java | 6 + .../data/types/apiimpl/ApiInterfaceImpl.java | 14 ++ .../classes/AbstractSomeAbstractClass.java | 9 + .../data/types/classes/SomeAbstractClass.java | 12 + .../types/classes/SomeAbstractClassImpl.java | 8 + .../java/data/types/fields/FieldHolder.java | 24 ++ .../java/data/types/hierarchy/Entity.java | 6 + .../java/data/types/hierarchy/Player.java | 41 ++++ .../data/types/hierarchy/loc/Location.java | 41 ++++ .../data/types/hierarchy/loc/Position.java | 12 + .../types/hierarchy/loc/PositionImpl.java | 8 + .../data/types/rename/RenamedTestEnum.java | 16 ++ .../data/types/rename/TestAnnotation.java | 14 ++ settings.gradle.kts | 3 + 69 files changed, 2148 insertions(+) create mode 100644 build-logic/src/main/kotlin/config-java25.gradle.kts create mode 100644 classfile-utils/build.gradle.kts create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/ClassfileUtils.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/MethodType.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/MethodDescriptorPredicate.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/action/DirectStaticCall.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/action/MethodRewriteAction.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/package-info.java create mode 100644 classfile-utils/src/mainForNewTargets/java/io/papermc/asm/rules/classes/LegacyEnum.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/TestUtil.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/TransformerTest.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/checks/ExecutionTransformerCheck.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/checks/RewriteTransformerCheck.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerCheck.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/checks/TransformerChecksProvider.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/checks/package-info.java create mode 100644 classfile-utils/src/testData/java/data/classes/ClassToInterfaceRedirectUser.java create mode 100644 classfile-utils/src/testData/java/data/classes/ClassToInterfaceUser.java create mode 100644 classfile-utils/src/testData/java/data/classes/EnumToInterfaceUser.java create mode 100644 classfile-utils/src/testData/java/data/fields/FieldToMethodSameOwnerUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/Methods.java create mode 100644 classfile-utils/src/testData/java/data/methods/inplace/SubTypeReturnUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/inplace/SuperTypeParamUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/statics/MoveToInstanceUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/statics/PlainUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/statics/param/ParamDirectUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/statics/param/ParamFuzzyUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectUser.java create mode 100644 classfile-utils/src/testData/java/data/methods/statics/returns/ReturnDirectWithContextUser.java create mode 100644 classfile-utils/src/testData/java/data/rename/RenameTest.java create mode 100644 classfile-utils/src/testData/java/data/types/apiimpl/ApiEnum.java create mode 100644 classfile-utils/src/testData/java/data/types/apiimpl/ApiInterface.java create mode 100644 classfile-utils/src/testData/java/data/types/apiimpl/ApiInterfaceImpl.java create mode 100644 classfile-utils/src/testData/java/data/types/classes/SomeAbstractClass.java create mode 100644 classfile-utils/src/testData/java/data/types/classes/SomeAbstractClassImpl.java create mode 100644 classfile-utils/src/testData/java/data/types/fields/FieldHolder.java create mode 100644 classfile-utils/src/testData/java/data/types/hierarchy/Entity.java create mode 100644 classfile-utils/src/testData/java/data/types/hierarchy/Mob.java create mode 100644 classfile-utils/src/testData/java/data/types/hierarchy/Player.java create mode 100644 classfile-utils/src/testData/java/data/types/hierarchy/loc/Location.java create mode 100644 classfile-utils/src/testData/java/data/types/hierarchy/loc/Position.java create mode 100644 classfile-utils/src/testData/java/data/types/hierarchy/loc/PositionImpl.java create mode 100644 classfile-utils/src/testData/java/data/types/rename/TestAnnotation.java create mode 100644 classfile-utils/src/testData/java/data/types/rename/TestEnum.java create mode 100644 classfile-utils/src/testData/resources/expected/data/methods/statics/PlainUser.class create mode 100644 classfile-utils/src/testDataNewTargets/java/data/SameClassTarget.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/methods/Methods.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnum.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiEnumImpl.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterface.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/apiimpl/ApiInterfaceImpl.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/classes/AbstractSomeAbstractClass.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClass.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/classes/SomeAbstractClassImpl.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/fields/FieldHolder.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Location.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/Position.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/loc/PositionImpl.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/rename/TestAnnotation.java 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..1e45b5a --- /dev/null +++ b/build-logic/src/main/kotlin/config-java25.gradle.kts @@ -0,0 +1,66 @@ +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() +} + +dependencies { + compileOnlyApi(libs.jspecify) + testCompileOnly(libs.jspecify) + compileOnly(libs.jetbrainsAnnotations) + testCompileOnly(libs.jetbrainsAnnotations) + + testImplementation(libs.jupiterApi) + testImplementation(libs.jupiterParams) + testRuntimeOnly(libs.jupiterEngine) + testRuntimeOnly(libs.platformLauncher) +} + +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..0825f01 --- /dev/null +++ b/classfile-utils/build.gradle.kts @@ -0,0 +1,86 @@ +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) + + 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/ClassfileUtils.java b/classfile-utils/src/main/java/io/papermc/classfile/ClassfileUtils.java new file mode 100644 index 0000000..9e298b0 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/ClassfileUtils.java @@ -0,0 +1,26 @@ +package io.papermc.classfile; + +import java.lang.constant.ClassDesc; + +public final class ClassfileUtils { + + private ClassfileUtils() { + } + + public static ClassDesc desc(final Class clazz) { + return clazz.describeConstable().orElseThrow(); + } + + public static boolean startsWith(final CharSequence text, final char[] prefix) { + final int len = prefix.length; + if (text.length() < len) { + return false; + } + for (int i = 0; i < len; i++) { + if (text.charAt(i) != prefix[i]) { + return false; + } + } + return true; + } +} 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..47570fe --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java @@ -0,0 +1,61 @@ +package io.papermc.classfile; + +import io.papermc.classfile.method.MethodRewrite; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeModel; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.MethodModel; +import java.lang.classfile.MethodTransform; +import java.util.List; +import java.util.function.Predicate; + +public class RewriteProcessor { + + private static final ClassFile CLASS_FILE = ClassFile.of(); + + private final List ctorRewrites; + + private final CodeTransform normalTransform; + private final ClassTransform transform; + + public RewriteProcessor(final List methodRewrites) { + this.ctorRewrites = methodRewrites.stream().filter(MethodRewrite::requiresMethodTransform).toList(); + final List normalRewrites = methodRewrites.stream().filter(Predicate.not(MethodRewrite::requiresMethodTransform)).toList(); + + // TODO look into this andThen issue. I think it causes way too many calls. + // it seems to send the corrected instruction through all the other transforms as well. + this.normalTransform = normalRewrites.stream().reduce(CodeTransform.ACCEPT_ALL, CodeTransform::andThen, CodeTransform::andThen); + this.transform = this.buildTransform(); + } + + private ClassTransform buildTransform() { + if (this.ctorRewrites.isEmpty()) { + return ClassTransform.transformingMethodBodies(this.normalTransform); + } else { + return (classBuilder, classElement) -> { + if (classElement instanceof final MethodModel method) { + classBuilder.transformMethod(method, (methodBuilder, methodElement) -> { + if (methodElement instanceof final CodeModel code) { + final CodeTransform perMethod = this.ctorRewrites.stream() + .map(MethodRewrite::newConstructorTransform) + .reduce(this.normalTransform, CodeTransform::andThen); + methodBuilder.transformCode(code, perMethod); + } else { + methodBuilder.with(methodElement); + } + }); + } else { + classBuilder.with(classElement); + } + }; + } + } + + public byte[] rewrite(final byte[] input) { + final ClassModel inputModel = CLASS_FILE.parse(input); + return CLASS_FILE.transformClass(inputModel, this.transform); + } +} 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..44b4323 --- /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 ReturnType(returnType); + } + + static MethodDescriptorPredicate hasParameter(final ClassDesc parameterType) { + return new HasParameter(parameterType); + } + + record ReturnType(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..24bce28 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java @@ -0,0 +1,65 @@ +package io.papermc.classfile.method; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +import static io.papermc.classfile.ClassfileUtils.startsWith; + +public sealed interface MethodNamePredicate extends Predicate { + + static MethodNamePredicate anyNonConstructors() { + final class Holder { + static final MethodNamePredicate INSTANCE = new Any(); + } + return Holder.INSTANCE; + } + + static MethodNamePredicate constructor() { + return exact(""); + } + + 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.toCharArray()); + } + + record ExactMatch(List names) implements MethodNamePredicate { + + public ExactMatch { + names = List.copyOf(names); + } + + @Override + public boolean test(final CharSequence s) { + return this.names.stream().anyMatch(name -> name.contentEquals(s)); + } + } + + record PrefixMatch(char[] prefix) implements MethodNamePredicate { + + @Override + public boolean test(final CharSequence s) { + return startsWith(s, this.prefix); + } + } + + record Any() implements MethodNamePredicate { + + @Override + public boolean test(final CharSequence charSequence) { + return true; + } + } +} 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..c105682 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java @@ -0,0 +1,161 @@ +package io.papermc.classfile.method; + +import io.papermc.classfile.method.action.MethodRewriteAction; +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.InvokeDynamicInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.NewObjectInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.invoke.LambdaMetafactory; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +import static io.papermc.classfile.ClassfileUtils.desc; + +public record MethodRewrite(ClassDesc owner, MethodNamePredicate methodName, MethodDescriptorPredicate descriptor, MethodRewriteAction action) implements CodeTransform { + + private static final ClassDesc LAMBDA_METAFACTORY = desc(LambdaMetafactory.class); + + public boolean requiresMethodTransform() { + return this.methodName().test(""); + } + + boolean transformInvoke(final CodeBuilder builder, final InvokeInstruction invoke) { + final ClassDesc methodOwner = invoke.owner().asSymbol(); + if (!this.owner.equals(methodOwner)) { + return false; + } + if (!this.methodName.test(invoke.name())) { + return false; + } + if (!this.descriptor.test(invoke.typeSymbol())) { + return false; + } + if (invoke.opcode() == Opcode.INVOKESPECIAL) { + throw new UnsupportedOperationException("You cannot redirect INVOKESPECIAL here"); + } + this.action.rewriteInvoke(builder::with, builder.constantPool(), invoke.opcode(), methodOwner, invoke.name().stringValue(), invoke.typeSymbol()); + return true; + } + + boolean transformInvokeDynamic(final CodeBuilder builder, 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 false; + } + if (!(args.get(1) instanceof final DirectMethodHandleDesc methodHandle)) { + return false; + } + if (!this.owner.equals(methodHandle.owner())) { + return false; + } + if (!this.methodName.test(methodHandle.methodName())) { + return false; + } + if (!this.descriptor.test(methodHandle.invocationType())) { + return false; + } + final MethodRewriteAction.BootstrapInfo info = new MethodRewriteAction.BootstrapInfo(bootstrapMethod, invokeDynamic.name().stringValue(), invokeDynamic.typeSymbol(), args); + this.action.rewriteInvokeDynamic(builder, + methodHandle.kind(), + methodHandle.owner(), + methodHandle.methodName(), + methodHandle.invocationType(), + info + ); + return true; + } + + @Override + public void accept(final CodeBuilder builder, final CodeElement element) { + final boolean written = switch (element) { + case final InvokeInstruction invoke -> this.transformInvoke(builder, invoke); + case final InvokeDynamicInstruction invokeDynamic -> this.transformInvokeDynamic(builder, invokeDynamic); + default -> false; + }; + if (!written) { + builder.with(element); + } + } + + public CodeTransform newConstructorTransform() { + return new ConstructorRewriteTransform(); + } + + private final class ConstructorRewriteTransform implements CodeTransform { + private final Deque> bufferStack = new ArrayDeque<>(); + + @Override + public void accept(final CodeBuilder builder, final CodeElement element) { + if (element instanceof NewObjectInstruction) { + // start of a constructor level + this.bufferStack.push(new ArrayList<>(List.of(element))); + return; + } + + if (!this.bufferStack.isEmpty()) { + this.bufferStack.peek().add(element); + + if (element instanceof final InvokeInstruction invoke && invoke.opcode() == Opcode.INVOKESPECIAL && invoke.name().equalsString(MethodRewriteAction.CONSTRUCTOR_METHOD_NAME)) { + // end of a constructor level + final List level = this.bufferStack.pop(); + + final List updatedLevel; + if (invoke.owner().matches(MethodRewrite.this.owner()) && MethodRewrite.this.methodName.test(invoke.name()) && MethodRewrite.this.descriptor.test(invoke.typeSymbol())) { + // matches our instruction to be removed + // we are removing the POP and NEW instructions here (first 2) + // AND the INVOKESPECIAL that was added a few lines above (last) + updatedLevel = new ArrayList<>(level.subList(2, level.size() - 1)); + MethodRewrite.this.action().rewriteInvoke( + updatedLevel::add, + builder.constantPool(), + invoke.opcode(), + invoke.owner().asSymbol(), + invoke.name().stringValue(), + invoke.typeSymbol() + ); + } else { + updatedLevel = level; + } + if (!this.bufferStack.isEmpty()) { + this.bufferStack.peek().addAll(updatedLevel); + } else { + this.flushLevel(builder, updatedLevel); + } + } + return; + } + this.writeToBuilder(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 -> this.flushLevel(builder, level)); + this.bufferStack.clear(); + } + + private void flushLevel(final CodeBuilder builder, final List level) { + level.forEach(el -> this.writeToBuilder(builder, el)); + } + + private void writeToBuilder(final CodeBuilder builder, final CodeElement element) { + // anytime we write to the builder, we first need to check that + // we don't need to also rewrite this instruction + MethodRewrite.this.accept(builder, element); + } + + } +} 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..0dea6df --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/DirectStaticCall.java @@ -0,0 +1,98 @@ +package io.papermc.classfile.method.action; + +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.Opcode; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +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.function.Consumer; +import org.jspecify.annotations.Nullable; + +/** + * 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 constructorMethodName 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 constructorMethodName) implements MethodRewriteAction { + + private static final String DEFAULT_CTOR_METHOD_PREFIX = "create"; + + public DirectStaticCall(final ClassDesc newOwner) { + this(newOwner, null); + } + + private String constructorStaticMethodName(final ClassDesc owner) { + if (this.constructorMethodName != null) { + return this.constructorMethodName; + } + // 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); + } + + @Override + public void rewriteInvoke(final Consumer emit, final ConstantPoolBuilder poolBuilder, final Opcode opcode, final ClassDesc owner, final String name, final MethodTypeDesc descriptor) { + MethodTypeDesc newDescriptor = descriptor; + if (opcode == Opcode.INVOKEVIRTUAL || opcode == Opcode.INVOKEINTERFACE) { + newDescriptor = descriptor.insertParameterTypes(0, owner); + } else if (opcode == Opcode.INVOKESPECIAL) { + if (CONSTRUCTOR_METHOD_NAME.equals(name)) { + newDescriptor = newDescriptor.changeReturnType(owner); + emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor))); + // builder.invokestatic(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor, false); + return; + } else { + throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); + } + } else if (opcode != Opcode.INVOKESTATIC) { + throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); + } + emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), name, newDescriptor))); + // builder.invokestatic(this.newOwner(), name, newDescriptor, false); + } + + @Override + public void rewriteInvokeDynamic(final CodeBuilder builder, + final DirectMethodHandleDesc.Kind kind, + final ClassDesc owner, + final String name, + final MethodTypeDesc descriptor, + final BootstrapInfo bootstrapInfo) { + MethodTypeDesc newDescriptor = descriptor; + final ConstantDesc[] newBootstrapArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); + if (kind == DirectMethodHandleDesc.Kind.INTERFACE_VIRTUAL || kind == DirectMethodHandleDesc.Kind.VIRTUAL) { + // TODO make sure we don't need this. The descriptor already seems to always have the "instance" as the first param if it exists + // newDescriptor = descriptor.insertParameterTypes(0, owner); + newBootstrapArgs[BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), name, newDescriptor); + } else if (kind == DirectMethodHandleDesc.Kind.SPECIAL || kind == DirectMethodHandleDesc.Kind.INTERFACE_SPECIAL || kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { + if (CONSTRUCTOR_METHOD_NAME.equals(name)) { + newDescriptor = newDescriptor.changeReturnType(owner); + newBootstrapArgs[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[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[BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), name, newDescriptor); + } + builder.invokedynamic(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..0a6401d --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/MethodRewriteAction.java @@ -0,0 +1,32 @@ +package io.papermc.classfile.method.action; + +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.Opcode; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.constant.ClassDesc; +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.function.Consumer; + +public sealed interface MethodRewriteAction permits DirectStaticCall { + + int BOOTSTRAP_HANDLE_IDX = 1; + int DYNAMIC_TYPE_IDX = 2; + String CONSTRUCTOR_METHOD_NAME = ""; + + void rewriteInvoke(Consumer emit, ConstantPoolBuilder poolBuilder, Opcode opcode, ClassDesc owner, String name, MethodTypeDesc descriptor); + + void rewriteInvokeDynamic(CodeBuilder builder, DirectMethodHandleDesc.Kind kind, ClassDesc owner, String name, MethodTypeDesc descriptor, 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/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/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/SimpleTest.java b/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java new file mode 100644 index 0000000..5ec2956 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java @@ -0,0 +1,38 @@ +package io.papermc.classfile; + +import data.methods.Methods; +import data.methods.Redirects; +import data.types.hierarchy.Entity; +import data.types.hierarchy.Player; +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 io.papermc.classfile.method.action.DirectStaticCall; +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.List; + +import static io.papermc.classfile.ClassfileUtils.desc; + +class SimpleTest { + + 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))); + } + + 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/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/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..e356d02 --- /dev/null +++ b/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java @@ -0,0 +1,42 @@ +package data.methods.statics; + +import data.methods.Methods; +import data.types.hierarchy.Entity; +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 Player(); + 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); + } +} 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..32f2fd1 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java @@ -0,0 +1,6 @@ +package data.types.hierarchy; + +public interface Entity { + + String getName(); +} 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..57533d9 --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Mob.java @@ -0,0 +1,9 @@ +package data.types.hierarchy; + +public class Mob implements Entity { + + @Override + public String getName() { + return "MOB"; + } +} 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..4ce766d --- /dev/null +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Player.java @@ -0,0 +1,31 @@ +package data.types.hierarchy; + +@SuppressWarnings("unused") +public class Player implements Entity { + + @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() { + } +} 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/statics/PlainUser.class b/classfile-utils/src/testData/resources/expected/data/methods/statics/PlainUser.class new file mode 100644 index 0000000000000000000000000000000000000000..ea3e3ada1413bc7439dbd27f753c2f3ee108e888 GIT binary patch literal 3279 zcmbVO+jA3D9R5x(**0C#OoJyXYxTI%Xfl#+$8g7q3eOG)$L6g9StmbM-cYaoPlGBE6Yj$RO zK6xsq&Fgj;3hL#fMTLr|1e&#Mw%>FOcV1v?SGuC8x1WlSNWnH05h>Uf2oAfNYh)Be z1vVDsXLNVk$~wsb@4bE0)@Em=?RKn|fvl-+%JY(@6s!|iw-oNMYa8ZNZ{EmdrKUQ> zR6LCh0!^;v?Fp>zil>)Lq{3K>P15RSfi+z#_hnS=*eW?X1Uh^Os)SI$pdHVs*p3|p zZ%TJPj|H~Sct!O5kfKazr;0AbnKLbu(Px>wj-tnT*T^L&^Jd01EHl|_^jW5ppV949 z3wC0cg56SbkHDrveAT!GI!hz;N+eYhvrmyR@5OTp_Nmy90|ML29`^uvbI4IqHUZA^5MHSQRxmF%dN$7`gSV2na>SkFl`4|9I)ih3N8s(QU#*YGA4MFTpctf+n!NAa>iBO4&c4wFSvLGY-b zcxL-JUQuvDiZLyfLN4+u7!cT8)^ARs5gpm;hIz)(ZGnb9E330v(}t-J=4U2!`?NNZ zBPE))GFom#vkkfSi4E?w!OU;<3c1(=E7YbKkZ!s*0}9O+#tI9wX7Q7e0)-({h0&ie zgs2igC3z4C2a{)?v|9nsVp-79Xuy2L%z9-fuzn@c8)5>=Rc}~T(Og8In%9;i$Z_kV zc&?I5OA&O}M5^+pk4_KGc309~47iH!67lg$;>D<{i2L1IUKPuRdrJBi9=7s!Mn7uE z3Tq9@aF0w853gR!avhiW`mz!1X=_9{0z=@%+TB?bmiR~n-8_`2Y(^eYriybg+CpgM z|B9s8D$c`*;6PgwRz)!PzxMoDUTlHo!BX!VVa(&2f;UyXg|`K^73#2XG#loeby-iQ zwV8>mMp122%eanydTH74Ir4KEs^k_?)S} z8AV9Wa|H(l#-F56%Q|TJ&CKu4_=qgqyVB@aH2QTEP3YmrttgstM3QZ;euyHBPJY~u zqJ=lay-4$nlRx?7C6TA^Ait7)3rR^VK>ShIKEEk09x}!u`rd@!a2jX0ir^kbFv=)TKx6!83DgA)U<~8rcW{WHpJlu^5GU_A zA-v0r<$2OV_?1|{hS!<+3)Iv@il^gZphF9ETnuz*fsTuTjtT0JXb)jEpRM$-jn*Pq z!@Fk;tFW2${k*C6V?Axen2Qaxxe=4xaj=CWrh^`C!!2Gs?_vkB=%jaD_?=$YBSW2a z$l@Ko<&~!$@8W%C^g2GkN7Sa1evPqTCGRArFkSe%#IMUfEf>(NfHv#X9Jt8)e0KuA TI|1J}0qwhh_G2J>5BdK9J~WP8 literal 0 HcmV?d00001 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..df234bd --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java @@ -0,0 +1,58 @@ +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) { + } + + 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..32f2fd1 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java @@ -0,0 +1,6 @@ +package data.types.hierarchy; + +public interface Entity { + + String getName(); +} 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..24ea6f1 --- /dev/null +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java @@ -0,0 +1,41 @@ +package data.types.hierarchy; + +@SuppressWarnings("unused") +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(); + } +} 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/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" From 7d964bd68e4eb40c03a4b08f0512a04353d3d60e Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sun, 1 Mar 2026 12:01:34 -0800 Subject: [PATCH 2/4] more work --- .../src/main/kotlin/config-java25.gradle.kts | 13 ++ .../{ClassfileUtils.java => ClassFiles.java} | 10 +- .../papermc/classfile/RewriteProcessor.java | 42 +--- .../classfile/method/MethodNamePredicate.java | 32 +-- .../classfile/method/MethodRewrite.java | 166 +++++----------- .../classfile/method/MethodRewriteIndex.java | 106 ++++++++++ .../method/action/DirectStaticCall.java | 64 +++--- .../method/action/MethodRewriteAction.java | 40 +++- .../ConstructorAwareCodeTransform.java | 150 ++++++++++++++ .../method/transform/MethodTransforms.java | 77 ++++++++ .../transform/SimpleMethodBodyTransform.java | 53 +++++ .../java/io/papermc/classfile/SimpleTest.java | 2 +- .../method/TestMethodRewriteIndex.java | 184 ++++++++++++++++++ .../method/action/TestDirectStaticCall.java | 39 ++++ .../TestConstructorAwareCodeTransform.java | 45 +++++ gradle/libs.versions.toml | 5 + 16 files changed, 828 insertions(+), 200 deletions(-) rename classfile-utils/src/main/java/io/papermc/classfile/{ClassfileUtils.java => ClassFiles.java} (60%) create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewriteIndex.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/ConstructorAwareCodeTransform.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransforms.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/SimpleMethodBodyTransform.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodRewriteIndex.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/action/TestDirectStaticCall.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/transform/TestConstructorAwareCodeTransform.java diff --git a/build-logic/src/main/kotlin/config-java25.gradle.kts b/build-logic/src/main/kotlin/config-java25.gradle.kts index 1e45b5a..69b12c9 100644 --- a/build-logic/src/main/kotlin/config-java25.gradle.kts +++ b/build-logic/src/main/kotlin/config-java25.gradle.kts @@ -49,18 +49,31 @@ 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/src/main/java/io/papermc/classfile/ClassfileUtils.java b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java similarity index 60% rename from classfile-utils/src/main/java/io/papermc/classfile/ClassfileUtils.java rename to classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java index 9e298b0..977e047 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/ClassfileUtils.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java @@ -1,10 +1,16 @@ package io.papermc.classfile; import java.lang.constant.ClassDesc; +import java.lang.invoke.LambdaMetafactory; -public final class ClassfileUtils { +public final class ClassFiles { - private ClassfileUtils() { + 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); + + private ClassFiles() { } public static ClassDesc desc(final Class clazz) { diff --git a/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java index 47570fe..285463f 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java @@ -1,57 +1,21 @@ package io.papermc.classfile; import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.MethodRewriteIndex; import java.lang.classfile.ClassFile; import java.lang.classfile.ClassModel; import java.lang.classfile.ClassTransform; -import java.lang.classfile.CodeElement; -import java.lang.classfile.CodeModel; -import java.lang.classfile.CodeTransform; -import java.lang.classfile.MethodModel; -import java.lang.classfile.MethodTransform; import java.util.List; -import java.util.function.Predicate; public class RewriteProcessor { private static final ClassFile CLASS_FILE = ClassFile.of(); - private final List ctorRewrites; - - private final CodeTransform normalTransform; private final ClassTransform transform; public RewriteProcessor(final List methodRewrites) { - this.ctorRewrites = methodRewrites.stream().filter(MethodRewrite::requiresMethodTransform).toList(); - final List normalRewrites = methodRewrites.stream().filter(Predicate.not(MethodRewrite::requiresMethodTransform)).toList(); - - // TODO look into this andThen issue. I think it causes way too many calls. - // it seems to send the corrected instruction through all the other transforms as well. - this.normalTransform = normalRewrites.stream().reduce(CodeTransform.ACCEPT_ALL, CodeTransform::andThen, CodeTransform::andThen); - this.transform = this.buildTransform(); - } - - private ClassTransform buildTransform() { - if (this.ctorRewrites.isEmpty()) { - return ClassTransform.transformingMethodBodies(this.normalTransform); - } else { - return (classBuilder, classElement) -> { - if (classElement instanceof final MethodModel method) { - classBuilder.transformMethod(method, (methodBuilder, methodElement) -> { - if (methodElement instanceof final CodeModel code) { - final CodeTransform perMethod = this.ctorRewrites.stream() - .map(MethodRewrite::newConstructorTransform) - .reduce(this.normalTransform, CodeTransform::andThen); - methodBuilder.transformCode(code, perMethod); - } else { - methodBuilder.with(methodElement); - } - }); - } else { - classBuilder.with(classElement); - } - }; - } + final MethodRewriteIndex methodIndex = new MethodRewriteIndex(methodRewrites); + this.transform = MethodRewrite.createTransform(methodIndex); } public byte[] rewrite(final byte[] input) { 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 index 24bce28..7baa1f6 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java @@ -1,25 +1,22 @@ 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; -import static io.papermc.classfile.ClassfileUtils.startsWith; +import static io.papermc.classfile.ClassFiles.startsWith; public sealed interface MethodNamePredicate extends Predicate { - static MethodNamePredicate anyNonConstructors() { + static MethodNamePredicate constructor() { final class Holder { - static final MethodNamePredicate INSTANCE = new Any(); + static final MethodNamePredicate INSTANCE = new Constructor(); } return Holder.INSTANCE; } - static MethodNamePredicate constructor() { - return exact(""); - } - static MethodNamePredicate exact(final String name, final String... otherNames) { final List names = new ArrayList<>(); names.add(name); @@ -38,6 +35,9 @@ static MethodNamePredicate prefix(final String 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 construtor predicate"); + } names = List.copyOf(names); } @@ -47,19 +47,25 @@ public boolean test(final CharSequence s) { } } - record PrefixMatch(char[] prefix) implements MethodNamePredicate { + record Constructor() implements MethodNamePredicate { @Override - public boolean test(final CharSequence s) { - return startsWith(s, this.prefix); + public boolean test(final CharSequence charSequence) { + return ClassFiles.CONSTRUCTOR_METHOD_NAME.contentEquals(charSequence); } } - record Any() implements MethodNamePredicate { + record PrefixMatch(char[] prefix) implements MethodNamePredicate { + + public PrefixMatch { + if (ClassFiles.startsWith(ClassFiles.CONSTRUCTOR_METHOD_NAME, prefix)) { + throw new IllegalArgumentException("Cannot use as a method name, use the dedicated construtor predicate"); + } + } @Override - public boolean test(final CharSequence charSequence) { - return true; + public boolean test(final CharSequence s) { + return startsWith(s, 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 index c105682..90b9c5f 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java @@ -1,64 +1,59 @@ 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.SimpleMethodBodyTransform; +import java.lang.classfile.ClassTransform; import java.lang.classfile.CodeBuilder; import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeModel; import java.lang.classfile.CodeTransform; +import java.lang.classfile.MethodModel; +import java.lang.classfile.MethodTransform; import java.lang.classfile.Opcode; +import java.lang.classfile.constantpool.ConstantPoolBuilder; import java.lang.classfile.instruction.InvokeDynamicInstruction; import java.lang.classfile.instruction.InvokeInstruction; -import java.lang.classfile.instruction.NewObjectInstruction; import java.lang.constant.ClassDesc; import java.lang.constant.ConstantDesc; import java.lang.constant.DirectMethodHandleDesc; import java.lang.invoke.LambdaMetafactory; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; import java.util.List; +import java.util.function.Consumer; -import static io.papermc.classfile.ClassfileUtils.desc; +import static io.papermc.classfile.ClassFiles.desc; -public record MethodRewrite(ClassDesc owner, MethodNamePredicate methodName, MethodDescriptorPredicate descriptor, MethodRewriteAction action) implements CodeTransform { +public record MethodRewrite(ClassDesc owner, MethodNamePredicate methodName, MethodDescriptorPredicate descriptor, MethodRewriteAction action) { - private static final ClassDesc LAMBDA_METAFACTORY = desc(LambdaMetafactory.class); - - public boolean requiresMethodTransform() { - return this.methodName().test(""); - } - - boolean transformInvoke(final CodeBuilder builder, final InvokeInstruction invoke) { - final ClassDesc methodOwner = invoke.owner().asSymbol(); - if (!this.owner.equals(methodOwner)) { - return false; - } - if (!this.methodName.test(invoke.name())) { + public boolean transformInvoke( + final Consumer emit, + final ConstantPoolBuilder poolBuilder, + final ClassDesc methodOwner, + final String methodName, + final InvokeInstruction invoke + ) { + // owner validated by caller + if (!this.methodName.test(methodName)) { return false; } if (!this.descriptor.test(invoke.typeSymbol())) { return false; } - if (invoke.opcode() == Opcode.INVOKESPECIAL) { - throw new UnsupportedOperationException("You cannot redirect INVOKESPECIAL here"); - } - this.action.rewriteInvoke(builder::with, builder.constantPool(), invoke.opcode(), methodOwner, invoke.name().stringValue(), invoke.typeSymbol()); + final CheckedConsumer checkedEmit = new CheckedConsumer<>(emit); + this.action.rewriteInvoke(checkedEmit, poolBuilder, invoke.opcode(), methodOwner, invoke.name().stringValue(), invoke.typeSymbol()); + checkedEmit.verify(); return true; } - boolean transformInvokeDynamic(final CodeBuilder builder, 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 false; - } - if (!(args.get(1) instanceof final DirectMethodHandleDesc methodHandle)) { - return false; - } - if (!this.owner.equals(methodHandle.owner())) { - return false; - } + public boolean transformInvokeDynamic( + final Consumer emit, + final ConstantPoolBuilder poolBuilder, + final DirectMethodHandleDesc bootstrapMethod, + final DirectMethodHandleDesc methodHandle, + final List args, + final InvokeDynamicInstruction invokeDynamic + ) { + // owner validated by caller if (!this.methodName.test(methodHandle.methodName())) { return false; } @@ -66,96 +61,41 @@ boolean transformInvokeDynamic(final CodeBuilder builder, final InvokeDynamicIns return false; } final MethodRewriteAction.BootstrapInfo info = new MethodRewriteAction.BootstrapInfo(bootstrapMethod, invokeDynamic.name().stringValue(), invokeDynamic.typeSymbol(), args); - this.action.rewriteInvokeDynamic(builder, - methodHandle.kind(), - methodHandle.owner(), - methodHandle.methodName(), - methodHandle.invocationType(), - info - ); + final CheckedConsumer checkedEmit = new CheckedConsumer<>(emit); + this.action.rewriteInvokeDynamic(checkedEmit, poolBuilder, methodHandle.kind(), methodHandle.owner(), methodHandle.methodName(), methodHandle.invocationType(), info); + checkedEmit.verify(); return true; } - @Override - public void accept(final CodeBuilder builder, final CodeElement element) { - final boolean written = switch (element) { - case final InvokeInstruction invoke -> this.transformInvoke(builder, invoke); - case final InvokeDynamicInstruction invokeDynamic -> this.transformInvokeDynamic(builder, invokeDynamic); - default -> false; - }; - if (!written) { - builder.with(element); + public static ClassTransform createTransform(final MethodRewriteIndex index) { + final SimpleMethodBodyTransform basicTransform = new SimpleMethodBodyTransform(index); + if (!index.hasConstructorRewrites()) { + return ClassTransform.transformingMethodBodies(basicTransform); } + return ClassTransform.transformingMethodBodies(CodeTransform.ofStateful(() -> { + return new ConstructorAwareCodeTransform(index, basicTransform); + })); } - public CodeTransform newConstructorTransform() { - return new ConstructorRewriteTransform(); - } - - private final class ConstructorRewriteTransform implements CodeTransform { - private final Deque> bufferStack = new ArrayDeque<>(); - - @Override - public void accept(final CodeBuilder builder, final CodeElement element) { - if (element instanceof NewObjectInstruction) { - // start of a constructor level - this.bufferStack.push(new ArrayList<>(List.of(element))); - return; - } - - if (!this.bufferStack.isEmpty()) { - this.bufferStack.peek().add(element); + private static final class CheckedConsumer implements Consumer { - if (element instanceof final InvokeInstruction invoke && invoke.opcode() == Opcode.INVOKESPECIAL && invoke.name().equalsString(MethodRewriteAction.CONSTRUCTOR_METHOD_NAME)) { - // end of a constructor level - final List level = this.bufferStack.pop(); + private final Consumer wrapped; + boolean called = false; - final List updatedLevel; - if (invoke.owner().matches(MethodRewrite.this.owner()) && MethodRewrite.this.methodName.test(invoke.name()) && MethodRewrite.this.descriptor.test(invoke.typeSymbol())) { - // matches our instruction to be removed - // we are removing the POP and NEW instructions here (first 2) - // AND the INVOKESPECIAL that was added a few lines above (last) - updatedLevel = new ArrayList<>(level.subList(2, level.size() - 1)); - MethodRewrite.this.action().rewriteInvoke( - updatedLevel::add, - builder.constantPool(), - invoke.opcode(), - invoke.owner().asSymbol(), - invoke.name().stringValue(), - invoke.typeSymbol() - ); - } else { - updatedLevel = level; - } - if (!this.bufferStack.isEmpty()) { - this.bufferStack.peek().addAll(updatedLevel); - } else { - this.flushLevel(builder, updatedLevel); - } - } - return; - } - this.writeToBuilder(builder, element); + private CheckedConsumer(final Consumer wrapped) { + this.wrapped = wrapped; } @Override - public void atEnd(final CodeBuilder builder) { - // Drain stack bottom-up - final List> remaining = new ArrayList<>(this.bufferStack); - Collections.reverse(remaining); - remaining.forEach(level -> this.flushLevel(builder, level)); - this.bufferStack.clear(); - } - - private void flushLevel(final CodeBuilder builder, final List level) { - level.forEach(el -> this.writeToBuilder(builder, el)); + public void accept(final T t) { + this.wrapped.accept(t); + this.called = true; } - private void writeToBuilder(final CodeBuilder builder, final CodeElement element) { - // anytime we write to the builder, we first need to check that - // we don't need to also rewrite this instruction - MethodRewrite.this.accept(builder, element); + public void verify() { + if (!this.called) { + throw new IllegalStateException("Consumer was not called"); + } } - } } 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 index 0dea6df..21ddf21 100644 --- 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 @@ -1,9 +1,10 @@ package io.papermc.classfile.method.action; -import java.lang.classfile.CodeBuilder; +import io.papermc.classfile.ClassFiles; import java.lang.classfile.CodeElement; import java.lang.classfile.Opcode; 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; @@ -20,10 +21,10 @@ * for rewriting both standard invoke instructions and dynamic invocations. * * @param newOwner The target class (owner) for the rewritten method call. - * @param constructorMethodName The method name to be used if this action represents a constructor 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 constructorMethodName) implements MethodRewriteAction { +public record DirectStaticCall(ClassDesc newOwner, @Nullable String staticMethodName) implements MethodRewriteAction { private static final String DEFAULT_CTOR_METHOD_PREFIX = "create"; @@ -32,24 +33,37 @@ public DirectStaticCall(final ClassDesc newOwner) { } private String constructorStaticMethodName(final ClassDesc owner) { - if (this.constructorMethodName != null) { - return this.constructorMethodName; + if (this.staticMethodName != null) { + return this.staticMethodName; } // 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); } + private String staticMethodName(final String originalName) { + if (this.staticMethodName != null) { + return this.staticMethodName; + } + return originalName; + } + @Override - public void rewriteInvoke(final Consumer emit, final ConstantPoolBuilder poolBuilder, final Opcode opcode, final ClassDesc owner, final String name, final MethodTypeDesc descriptor) { + public void rewriteInvoke( + final Consumer emit, + final ConstantPoolBuilder poolBuilder, + final Opcode opcode, + final ClassDesc owner, + final String name, + final MethodTypeDesc descriptor + ) { MethodTypeDesc newDescriptor = descriptor; if (opcode == Opcode.INVOKEVIRTUAL || opcode == Opcode.INVOKEINTERFACE) { newDescriptor = descriptor.insertParameterTypes(0, owner); } else if (opcode == Opcode.INVOKESPECIAL) { - if (CONSTRUCTOR_METHOD_NAME.equals(name)) { + if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { newDescriptor = newDescriptor.changeReturnType(owner); emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor))); - // builder.invokestatic(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor, false); return; } else { throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); @@ -57,33 +71,31 @@ public void rewriteInvoke(final Consumer emit, final ConstantPoolBu } else if (opcode != Opcode.INVOKESTATIC) { throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); } - emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), name, newDescriptor))); - // builder.invokestatic(this.newOwner(), name, newDescriptor, false); + emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), this.staticMethodName(name), newDescriptor))); } @Override - public void rewriteInvokeDynamic(final CodeBuilder builder, - final DirectMethodHandleDesc.Kind kind, - final ClassDesc owner, - final String name, - final MethodTypeDesc descriptor, - final BootstrapInfo bootstrapInfo) { + public void rewriteInvokeDynamic( + final Consumer emit, + final ConstantPoolBuilder poolBuilder, + final DirectMethodHandleDesc.Kind kind, + final ClassDesc owner, + final String name, + final MethodTypeDesc descriptor, + final BootstrapInfo bootstrapInfo + ) { MethodTypeDesc newDescriptor = descriptor; final ConstantDesc[] newBootstrapArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); if (kind == DirectMethodHandleDesc.Kind.INTERFACE_VIRTUAL || kind == DirectMethodHandleDesc.Kind.VIRTUAL) { // TODO make sure we don't need this. The descriptor already seems to always have the "instance" as the first param if it exists // newDescriptor = descriptor.insertParameterTypes(0, owner); - newBootstrapArgs[BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), name, newDescriptor); + newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.staticMethodName(name), newDescriptor); } else if (kind == DirectMethodHandleDesc.Kind.SPECIAL || kind == DirectMethodHandleDesc.Kind.INTERFACE_SPECIAL || kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { - if (CONSTRUCTOR_METHOD_NAME.equals(name)) { + if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { newDescriptor = newDescriptor.changeReturnType(owner); - newBootstrapArgs[BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, - this.newOwner(), - this.constructorStaticMethodName(owner), - newDescriptor - ); + 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[DYNAMIC_TYPE_IDX] = newDescriptor; + newBootstrapArgs[ClassFiles.DYNAMIC_TYPE_IDX] = newDescriptor; } else { throw new UnsupportedOperationException("Unhandled static rewrite: " + kind + " " + owner + " " + name + " " + descriptor); } @@ -91,8 +103,8 @@ public void rewriteInvokeDynamic(final CodeBuilder builder, throw new UnsupportedOperationException("Unhandled static rewrite: " + kind + " " + owner + " " + name + " " + descriptor); } else { // is a static method - newBootstrapArgs[BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), name, newDescriptor); + newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.staticMethodName(name), newDescriptor); } - builder.invokedynamic(bootstrapInfo.create(newBootstrapArgs)); + emit.accept(InvokeDynamicInstruction.of(poolBuilder.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 index 0a6401d..7165b4b 100644 --- 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 @@ -1,6 +1,5 @@ package io.papermc.classfile.method.action; -import java.lang.classfile.CodeBuilder; import java.lang.classfile.CodeElement; import java.lang.classfile.Opcode; import java.lang.classfile.constantpool.ConstantPoolBuilder; @@ -14,13 +13,42 @@ public sealed interface MethodRewriteAction permits DirectStaticCall { - int BOOTSTRAP_HANDLE_IDX = 1; - int DYNAMIC_TYPE_IDX = 2; - String CONSTRUCTOR_METHOD_NAME = ""; - + /** + * Rewrites a method invocation instruction, modifying the method owner, + * name, and descriptor, and emits the modified instruction. + * + * @param emit A consumer that accepts the newly created {@code CodeElement}. + * This is used to emit the rewritten instruction. **MUST** be called or exception thrown. + * @param poolBuilder The {@code ConstantPoolBuilder} used to manage and create + * constant pool entries required by the instruction. + * @param opcode The original {@code Opcode} of the method invocation + * (e.g., {@code INVOKEVIRTUAL}, {@code INVOKESTATIC}). + * @param owner The {@code ClassDesc} representing the class that owns the original method. + * @param name The name of the method being invoked. + * @param descriptor The {@code MethodTypeDesc} describing the method's parameters + * and return type. + */ void rewriteInvoke(Consumer emit, ConstantPoolBuilder poolBuilder, Opcode opcode, ClassDesc owner, String name, MethodTypeDesc descriptor); - void rewriteInvokeDynamic(CodeBuilder builder, DirectMethodHandleDesc.Kind kind, ClassDesc owner, String name, MethodTypeDesc descriptor, BootstrapInfo bootstrapInfo); + /** + * 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 emit A consumer that accepts the newly created {@code CodeElement}. + * This is used to emit the rewritten invokedynamic instruction. + * **MUST** be called or an exception should be thrown. + * @param poolBuilder The {@code ConstantPoolBuilder} used to manage and create + * constant pool entries required by the invokedynamic instruction. + * @param kind The {@code DirectMethodHandleDesc.Kind} indicating the kind of method handle + * associated with the bootstrap method. + * @param owner The {@code ClassDesc} representing the class that owns the invokedynamic call site. + * @param name The name of the method or call site being referenced by the invokedynamic instruction. + * @param descriptor The {@code MethodTypeDesc} describing the parameters and return type of the 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(Consumer emit, ConstantPoolBuilder poolBuilder, DirectMethodHandleDesc.Kind kind, ClassDesc owner, String name, MethodTypeDesc descriptor, BootstrapInfo bootstrapInfo); record BootstrapInfo(DirectMethodHandleDesc method, String invocationName, MethodTypeDesc invocationType, List args) { 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..ed43038 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/ConstructorAwareCodeTransform.java @@ -0,0 +1,150 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.ClassFiles; +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.MethodRewriteIndex; +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; + + public ConstructorAwareCodeTransform(final MethodRewriteIndex index, final CodeTransform fallbackTransform) { + this.index = index; + this.fallbackTransform = fallbackTransform; + } + + @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); + 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/MethodTransforms.java b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransforms.java new file mode 100644 index 0000000..fac2e17 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransforms.java @@ -0,0 +1,77 @@ +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.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.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 ClassDesc owner; + final String methodName; + final Writer rewriter; + if (element instanceof final InvokeInstruction invoke) { + owner = invoke.owner().asSymbol(); + methodName = invoke.name().stringValue(); + rewriter = (emit, poolBuilder, rewrite) -> rewrite.transformInvoke(emit, poolBuilder, owner, methodName, invoke); + } 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(); + rewriter = (emit, poolBuilder, rewrite) -> rewrite.transformInvokeDynamic(emit, poolBuilder, bootstrapMethod, methodHandle, args, invokeDynamic); + } else { + return null; + } + return new BoundRewrite(rewriter, owner, methodName); + } + + record BoundRewrite(Writer writer, ClassDesc owner, String methodName) { + + public boolean tryWrite(final Consumer emit, final ConstantPoolBuilder poolBuilder, final MethodRewrite methodRewrite) { + return this.writer.write(emit, poolBuilder, methodRewrite); + } + } + + @FunctionalInterface + interface Writer { + boolean write(Consumer emit, ConstantPoolBuilder poolBuilder, 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..7605378 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/SimpleMethodBodyTransform.java @@ -0,0 +1,53 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.MethodRewriteIndex; +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; + +// won't work if we are rewriting any constructors that aren't lambdas + +/** + * 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; + + public SimpleMethodBodyTransform(final MethodRewriteIndex index) { + this.index = index; + } + + @Override + public void accept(final CodeBuilder builder, final CodeElement element) { + final MethodTransforms.BoundRewrite boundRewrite = MethodTransforms.setupRewrite(element); + if (boundRewrite == null) { + builder.with(element); + return; + } + final List candidates = this.index.candidates(boundRewrite.owner(), boundRewrite.methodName()); + 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/test/java/io/papermc/classfile/SimpleTest.java b/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java index 5ec2956..2862e74 100644 --- a/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java +++ b/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java @@ -13,7 +13,7 @@ import java.util.ArrayList; import java.util.List; -import static io.papermc.classfile.ClassfileUtils.desc; +import static io.papermc.classfile.ClassFiles.desc; class SimpleTest { 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..b6a0d60 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestDirectStaticCall.java @@ -0,0 +1,39 @@ +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))); + } + + 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/transform/TestConstructorAwareCodeTransform.java b/classfile-utils/src/test/java/io/papermc/classfile/method/transform/TestConstructorAwareCodeTransform.java new file mode 100644 index 0000000..a8ccc0d --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/transform/TestConstructorAwareCodeTransform.java @@ -0,0 +1,45 @@ +package io.papermc.classfile.method.transform; + +import io.papermc.classfile.ClassFiles; +import java.lang.classfile.CodeElement; +import java.lang.classfile.Opcode; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.NewObjectInstruction; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TestConstructorAwareCodeTransform { + + @Test + void nonInvokeInstructionReturnsFalse() { + final CodeElement element = mock(NewObjectInstruction.class); + assertThat(ConstructorAwareCodeTransform.isConstructor(element)).isFalse(); + } + + @Test + void invokespecialInitReturnsTrue() { + final InvokeInstruction invoke = mock(InvokeInstruction.class, RETURNS_DEEP_STUBS); + when(invoke.opcode()).thenReturn(Opcode.INVOKESPECIAL); + when(invoke.method().name().equalsString(ClassFiles.CONSTRUCTOR_METHOD_NAME)).thenReturn(true); + assertThat(ConstructorAwareCodeTransform.isConstructor(invoke)).isTrue(); + } + + @Test + void invokespecialNonInitReturnsFalse() { + final InvokeInstruction invoke = mock(InvokeInstruction.class, RETURNS_DEEP_STUBS); + when(invoke.opcode()).thenReturn(Opcode.INVOKESPECIAL); + when(invoke.method().name().equalsString(ClassFiles.CONSTRUCTOR_METHOD_NAME)).thenReturn(false); + assertThat(ConstructorAwareCodeTransform.isConstructor(invoke)).isFalse(); + } + + @Test + void invokevirtualInitReturnsFalse() { + final InvokeInstruction invoke = mock(InvokeInstruction.class, RETURNS_DEEP_STUBS); + when(invoke.opcode()).thenReturn(Opcode.INVOKEVIRTUAL); + assertThat(ConstructorAwareCodeTransform.isConstructor(invoke)).isFalse(); + } +} 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" } From 7eb2dd16ebefbd605e275e97162e3fd97e4b956f Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sun, 1 Mar 2026 18:29:16 -0800 Subject: [PATCH 3/4] SubtypeReturn and WrapReturnValue --- classfile-utils/build.gradle.kts | 2 + .../java/io/papermc/classfile/ClassFiles.java | 18 +-- .../papermc/classfile/RewriteProcessor.java | 14 ++- .../classfile/method/MethodNamePredicate.java | 26 ++--- .../classfile/method/MethodRewrite.java | 95 ++++++---------- .../method/action/DirectStaticCall.java | 45 ++++---- .../method/action/MethodRewriteAction.java | 45 ++++---- .../method/action/SubtypeReturn.java | 59 ++++++++++ .../method/action/WrapReturnValue.java | 104 ++++++++++++++++++ .../transform/BridgeMethodRegistry.java | 62 +++++++++++ .../ConstructorAwareCodeTransform.java | 7 +- .../transform/MethodTransformContext.java | 81 ++++++++++++++ .../transform/MethodTransformContextImpl.java | 26 +++++ .../method/transform/MethodTransforms.java | 25 +++-- .../transform/SimpleMethodBodyTransform.java | 11 +- .../method/transform/TrackingConsumer.java | 40 +++++++ .../classfile/transform/TransformContext.java | 15 +++ .../transform/TransformContextImpl.java | 7 ++ .../java/io/papermc/classfile/SimpleTest.java | 38 ------- .../method/TestMethodDescriptorPredicate.java | 76 +++++++++++++ .../method/TestMethodNamePredicate.java | 90 +++++++++++++++ .../method/action/TestDirectStaticCall.java | 4 + .../method/action/TestSubtypeReturn.java | 30 +++++ .../method/action/TestWrapReturnValue.java | 32 ++++++ .../TestConstructorAwareCodeTransform.java | 28 ++--- .../java/data/methods/statics/PlainUser.java | 22 +++- .../java/data/types/hierarchy/Entity.java | 8 ++ .../java/data/types/hierarchy/Mob.java | 19 ++++ .../java/data/types/hierarchy/Player.java | 19 ++++ .../methods/inplace/SubTypeReturnUser.class | Bin 0 -> 2170 bytes .../data/methods/statics/PlainUser.class | Bin 3279 -> 4300 bytes .../statics/returns/ReturnDirectUser.class | Bin 0 -> 2397 bytes .../java/data/methods/Redirects.java | 4 + .../java/data/types/hierarchy/Entity.java | 8 ++ .../java/data/types/hierarchy/Mob.java | 37 +++++++ .../java/data/types/hierarchy/Player.java | 31 +++++- 36 files changed, 925 insertions(+), 203 deletions(-) create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/action/SubtypeReturn.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapReturnValue.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/BridgeMethodRegistry.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContext.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContextImpl.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/transform/TrackingConsumer.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContext.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/transform/TransformContextImpl.java delete mode 100644 classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodDescriptorPredicate.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/TestMethodNamePredicate.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSubtypeReturn.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapReturnValue.java create mode 100644 classfile-utils/src/testData/resources/expected/data/methods/inplace/SubTypeReturnUser.class create mode 100644 classfile-utils/src/testData/resources/expected/data/methods/statics/returns/ReturnDirectUser.class create mode 100644 classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Mob.java diff --git a/classfile-utils/build.gradle.kts b/classfile-utils/build.gradle.kts index 0825f01..6263ba1 100644 --- a/classfile-utils/build.gradle.kts +++ b/classfile-utils/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { 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) } diff --git a/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java index 977e047..0e9e033 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java @@ -1,7 +1,11 @@ package io.papermc.classfile; import java.lang.constant.ClassDesc; +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 { @@ -17,16 +21,12 @@ public static ClassDesc desc(final Class clazz) { return clazz.describeConstable().orElseThrow(); } - public static boolean startsWith(final CharSequence text, final char[] prefix) { - final int len = prefix.length; - if (text.length() < len) { - return false; - } - for (int i = 0; i < len; i++) { - if (text.charAt(i) != prefix[i]) { - return false; + 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 true; + return descriptor; } } diff --git a/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java index 285463f..54d25e4 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/RewriteProcessor.java @@ -2,6 +2,8 @@ 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; @@ -11,15 +13,19 @@ public class RewriteProcessor { private static final ClassFile CLASS_FILE = ClassFile.of(); - private final ClassTransform transform; + private final MethodRewriteIndex methodIndex; public RewriteProcessor(final List methodRewrites) { - final MethodRewriteIndex methodIndex = new MethodRewriteIndex(methodRewrites); - this.transform = MethodRewrite.createTransform(methodIndex); + this.methodIndex = new MethodRewriteIndex(methodRewrites); } public byte[] rewrite(final byte[] input) { final ClassModel inputModel = CLASS_FILE.parse(input); - return CLASS_FILE.transformClass(inputModel, this.transform); + 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/method/MethodNamePredicate.java b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java index 7baa1f6..24ac550 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodNamePredicate.java @@ -6,9 +6,7 @@ import java.util.List; import java.util.function.Predicate; -import static io.papermc.classfile.ClassFiles.startsWith; - -public sealed interface MethodNamePredicate extends Predicate { +public sealed interface MethodNamePredicate extends Predicate { static MethodNamePredicate constructor() { final class Holder { @@ -29,43 +27,43 @@ static MethodNamePredicate exact(final Collection names) { } static MethodNamePredicate prefix(final String prefix) { - return new PrefixMatch(prefix.toCharArray()); + 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 construtor predicate"); + throw new IllegalArgumentException("Cannot use as a method name, use the dedicated constructor predicate"); } names = List.copyOf(names); } @Override - public boolean test(final CharSequence s) { - return this.names.stream().anyMatch(name -> name.contentEquals(s)); + public boolean test(final String s) { + return this.names.stream().anyMatch(s::equals); } } record Constructor() implements MethodNamePredicate { @Override - public boolean test(final CharSequence charSequence) { - return ClassFiles.CONSTRUCTOR_METHOD_NAME.contentEquals(charSequence); + public boolean test(final String charSequence) { + return ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(charSequence); } } - record PrefixMatch(char[] prefix) implements MethodNamePredicate { + record PrefixMatch(String prefix) implements MethodNamePredicate { public PrefixMatch { - if (ClassFiles.startsWith(ClassFiles.CONSTRUCTOR_METHOD_NAME, prefix)) { - throw new IllegalArgumentException("Cannot use as a method name, use the dedicated construtor predicate"); + 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 CharSequence s) { - return startsWith(s, this.prefix); + 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 index 90b9c5f..57f1752 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodRewrite.java @@ -2,100 +2,77 @@ 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 java.lang.classfile.ClassTransform; -import java.lang.classfile.CodeBuilder; +import io.papermc.classfile.method.transform.TrackingConsumer; +import io.papermc.classfile.transform.TransformContext; import java.lang.classfile.CodeElement; -import java.lang.classfile.CodeModel; import java.lang.classfile.CodeTransform; -import java.lang.classfile.MethodModel; import java.lang.classfile.MethodTransform; import java.lang.classfile.Opcode; -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.invoke.LambdaMetafactory; +import java.lang.constant.MethodTypeDesc; import java.util.List; -import java.util.function.Consumer; - -import static io.papermc.classfile.ClassFiles.desc; public record MethodRewrite(ClassDesc owner, MethodNamePredicate methodName, MethodDescriptorPredicate descriptor, MethodRewriteAction action) { - public boolean transformInvoke( - final Consumer emit, - final ConstantPoolBuilder poolBuilder, - final ClassDesc methodOwner, - final String methodName, - final InvokeInstruction invoke - ) { + public MethodRewrite { + action.isValidFor(methodName, descriptor).ifPresent(IllegalArgumentException::new); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean doesMatch(final String name, final MethodTypeDesc descriptor) { + return this.methodName.test(name) && this.descriptor.test(descriptor); + } + + public boolean transformInvoke(final MethodTransformContext context, final Opcode opcode) { // owner validated by caller - if (!this.methodName.test(methodName)) { + if (!this.doesMatch(context.methodInfo().name(), context.methodInfo().descriptor())) { return false; } - if (!this.descriptor.test(invoke.typeSymbol())) { - return false; + try (TrackingConsumer.Instance _ = context.prime()) { + this.action.rewriteInvoke(context, opcode); } - final CheckedConsumer checkedEmit = new CheckedConsumer<>(emit); - this.action.rewriteInvoke(checkedEmit, poolBuilder, invoke.opcode(), methodOwner, invoke.name().stringValue(), invoke.typeSymbol()); - checkedEmit.verify(); return true; } public boolean transformInvokeDynamic( - final Consumer emit, - final ConstantPoolBuilder poolBuilder, + final MethodTransformContext context, final DirectMethodHandleDesc bootstrapMethod, final DirectMethodHandleDesc methodHandle, final List args, final InvokeDynamicInstruction invokeDynamic ) { // owner validated by caller - if (!this.methodName.test(methodHandle.methodName())) { - return false; - } - if (!this.descriptor.test(methodHandle.invocationType())) { + + // For VIRTUAL/INTERFACE_VIRTUAL, the descriptor includes the receiver as this first param. + // we need to remove that so that the descriptor matches a non-dynamic invocation, which is what our API expects for matching + final MethodTypeDesc descriptorForMatching = switch (methodHandle.kind()) { + case VIRTUAL, INTERFACE_VIRTUAL -> methodHandle.invocationType().dropParameterTypes(0, 1); + default -> methodHandle.invocationType(); + }; + if (!this.doesMatch(context.methodInfo().name(), descriptorForMatching)) { return false; } final MethodRewriteAction.BootstrapInfo info = new MethodRewriteAction.BootstrapInfo(bootstrapMethod, invokeDynamic.name().stringValue(), invokeDynamic.typeSymbol(), args); - final CheckedConsumer checkedEmit = new CheckedConsumer<>(emit); - this.action.rewriteInvokeDynamic(checkedEmit, poolBuilder, methodHandle.kind(), methodHandle.owner(), methodHandle.methodName(), methodHandle.invocationType(), info); - checkedEmit.verify(); + try (TrackingConsumer.Instance _ = context.prime()) { + this.action.rewriteInvokeDynamic(context, methodHandle.kind(), info); + } return true; } - public static ClassTransform createTransform(final MethodRewriteIndex index) { - final SimpleMethodBodyTransform basicTransform = new SimpleMethodBodyTransform(index); - if (!index.hasConstructorRewrites()) { - return ClassTransform.transformingMethodBodies(basicTransform); + 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 ClassTransform.transformingMethodBodies(CodeTransform.ofStateful(() -> { - return new ConstructorAwareCodeTransform(index, basicTransform); + return MethodTransform.transformingCode(CodeTransform.ofStateful(() -> { + return new ConstructorAwareCodeTransform(index, basicTransform, context); })); } - private static final class CheckedConsumer implements Consumer { - - private final Consumer wrapped; - boolean called = false; - - private CheckedConsumer(final Consumer wrapped) { - this.wrapped = wrapped; - } - - @Override - public void accept(final T t) { - this.wrapped.accept(t); - this.called = true; - } - - public void verify() { - if (!this.called) { - throw new IllegalStateException("Consumer was not called"); - } - } - } } 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 index 21ddf21..daa2e57 100644 --- 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 @@ -1,9 +1,10 @@ package io.papermc.classfile.method.action; import io.papermc.classfile.ClassFiles; -import java.lang.classfile.CodeElement; +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.constantpool.ConstantPoolBuilder; import java.lang.classfile.instruction.InvokeDynamicInstruction; import java.lang.classfile.instruction.InvokeInstruction; import java.lang.constant.ClassDesc; @@ -11,7 +12,7 @@ import java.lang.constant.DirectMethodHandleDesc; import java.lang.constant.MethodHandleDesc; import java.lang.constant.MethodTypeDesc; -import java.util.function.Consumer; +import java.util.Optional; import org.jspecify.annotations.Nullable; /** @@ -32,6 +33,11 @@ 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) { if (this.staticMethodName != null) { return this.staticMethodName; @@ -49,21 +55,17 @@ private String staticMethodName(final String originalName) { } @Override - public void rewriteInvoke( - final Consumer emit, - final ConstantPoolBuilder poolBuilder, - final Opcode opcode, - final ClassDesc owner, - final String name, - final MethodTypeDesc descriptor - ) { + 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(); MethodTypeDesc newDescriptor = descriptor; if (opcode == Opcode.INVOKEVIRTUAL || opcode == Opcode.INVOKEINTERFACE) { newDescriptor = descriptor.insertParameterTypes(0, owner); } else if (opcode == Opcode.INVOKESPECIAL) { if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { newDescriptor = newDescriptor.changeReturnType(owner); - emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor))); + context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor))); return; } else { throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); @@ -71,24 +73,17 @@ public void rewriteInvoke( } else if (opcode != Opcode.INVOKESTATIC) { throw new UnsupportedOperationException("Unhandled static rewrite: " + opcode + " " + owner + " " + name + " " + descriptor); } - emit.accept(InvokeInstruction.of(Opcode.INVOKESTATIC, poolBuilder.methodRefEntry(this.newOwner(), this.staticMethodName(name), newDescriptor))); + context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.newOwner(), this.staticMethodName(name), newDescriptor))); } @Override - public void rewriteInvokeDynamic( - final Consumer emit, - final ConstantPoolBuilder poolBuilder, - final DirectMethodHandleDesc.Kind kind, - final ClassDesc owner, - final String name, - final MethodTypeDesc descriptor, - final BootstrapInfo bootstrapInfo - ) { + 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(); MethodTypeDesc newDescriptor = descriptor; final ConstantDesc[] newBootstrapArgs = bootstrapInfo.args().toArray(new ConstantDesc[0]); if (kind == DirectMethodHandleDesc.Kind.INTERFACE_VIRTUAL || kind == DirectMethodHandleDesc.Kind.VIRTUAL) { - // TODO make sure we don't need this. The descriptor already seems to always have the "instance" as the first param if it exists - // newDescriptor = descriptor.insertParameterTypes(0, owner); newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.staticMethodName(name), newDescriptor); } else if (kind == DirectMethodHandleDesc.Kind.SPECIAL || kind == DirectMethodHandleDesc.Kind.INTERFACE_SPECIAL || kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { @@ -105,6 +100,6 @@ public void rewriteInvokeDynamic( // is a static method newBootstrapArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC, this.newOwner(), this.staticMethodName(name), newDescriptor); } - emit.accept(InvokeDynamicInstruction.of(poolBuilder.invokeDynamicEntry(bootstrapInfo.create(newBootstrapArgs)))); + 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 index 7165b4b..e28807d 100644 --- 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 @@ -1,54 +1,49 @@ package io.papermc.classfile.method.action; -import java.lang.classfile.CodeElement; +import io.papermc.classfile.method.MethodDescriptorPredicate; +import io.papermc.classfile.method.MethodNamePredicate; +import io.papermc.classfile.method.MethodRewrite; +import io.papermc.classfile.method.transform.MethodTransformContext; import java.lang.classfile.Opcode; -import java.lang.classfile.constantpool.ConstantPoolBuilder; -import java.lang.constant.ClassDesc; 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.function.Consumer; +import java.util.Optional; -public sealed interface MethodRewriteAction permits DirectStaticCall { +public sealed interface MethodRewriteAction permits DirectStaticCall, SubtypeReturn, 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 emit A consumer that accepts the newly created {@code CodeElement}. - * This is used to emit the rewritten instruction. **MUST** be called or exception thrown. - * @param poolBuilder The {@code ConstantPoolBuilder} used to manage and create - * constant pool entries required by the instruction. - * @param opcode The original {@code Opcode} of the method invocation - * (e.g., {@code INVOKEVIRTUAL}, {@code INVOKESTATIC}). - * @param owner The {@code ClassDesc} representing the class that owns the original method. - * @param name The name of the method being invoked. - * @param descriptor The {@code MethodTypeDesc} describing the method's parameters - * and return type. + * @param context The context containing information about the method invocation. + * @param opcode The opcode of the method invocation instruction. */ - void rewriteInvoke(Consumer emit, ConstantPoolBuilder poolBuilder, Opcode opcode, ClassDesc owner, String name, MethodTypeDesc descriptor); + 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 emit A consumer that accepts the newly created {@code CodeElement}. - * This is used to emit the rewritten invokedynamic instruction. - * **MUST** be called or an exception should be thrown. - * @param poolBuilder The {@code ConstantPoolBuilder} used to manage and create - * constant pool entries required by the invokedynamic instruction. + * @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 owner The {@code ClassDesc} representing the class that owns the invokedynamic call site. - * @param name The name of the method or call site being referenced by the invokedynamic instruction. - * @param descriptor The {@code MethodTypeDesc} describing the parameters and return type of the 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(Consumer emit, ConstantPoolBuilder poolBuilder, DirectMethodHandleDesc.Kind kind, ClassDesc owner, String name, MethodTypeDesc descriptor, BootstrapInfo bootstrapInfo); + void rewriteInvokeDynamic(MethodTransformContext context, DirectMethodHandleDesc.Kind kind, BootstrapInfo bootstrapInfo); record BootstrapInfo(DirectMethodHandleDesc method, String invocationName, MethodTypeDesc invocationType, List args) { 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..086a168 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/SubtypeReturn.java @@ -0,0 +1,59 @@ +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.ReturnType)) { + 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]); + // For VIRTUAL/INTERFACE_VIRTUAL kinds, descriptor (i.e., methodHandle.invocationType()) includes the receiver + // as the first parameter. But MethodHandleDesc.ofMethod for VIRTUAL adds the receiver itself, + // so we must strip it before constructing the new handle. + final MethodTypeDesc handleMethodType = switch (kind) { + case VIRTUAL, INTERFACE_VIRTUAL -> info.descriptor().dropParameterTypes(0, 1).changeReturnType(this.newReturnType); + default -> info.descriptor().changeReturnType(this.newReturnType); + }; + newArgs[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod(kind, info.owner(), info.name(), handleMethodType); + 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/WrapReturnValue.java b/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapReturnValue.java new file mode 100644 index 0000000..1a82fed --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapReturnValue.java @@ -0,0 +1,104 @@ +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.TypeKind; +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.Optional; + +/** + * 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.ReturnType)) { + return Optional.of("You must use a return descriptor predicate on " + descriptorPredicate); + } + // TODO maybe this works??? + // 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(); + // Emit the original call with the new return type + final MethodTypeDesc callDesc = info.descriptor().changeReturnType(this.newReturnType); + context.emitChangedDescriptor(opcode, callDesc); + // Emit the converter: converterOwner.converterMethod(newReturnType) -> original return type + final MethodTypeDesc converterDesc = MethodTypeDesc.of(info.descriptor().returnType(), this.newReturnType); + // TODO might want to, instead, change just the call to a generated method that handles this to avoid an extra call + context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.converterOwner, this.converterMethod, converterDesc))); + } + + @Override + public void rewriteInvokeDynamic(final MethodTransformContext context, final DirectMethodHandleDesc.Kind kind, final BootstrapInfo bootstrapInfo) { + final MethodTransformContext.MethodInfo info = context.methodInfo(); + // descriptor = invocationType of the method handle + // For VIRTUAL/INTERFACE_VIRTUAL, descriptor includes the receiver as the first param. + // The call descriptor strips the receiver and uses the newReturnType. + final MethodTypeDesc callDesc = switch (kind) { + case VIRTUAL, INTERFACE_VIRTUAL -> info.descriptor().dropParameterTypes(0, 1).changeReturnType(this.newReturnType); + default -> info.descriptor().changeReturnType(this.newReturnType); + }; + 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.name() + "$" + this.converterMethod, + info.descriptor(), + cb -> { + // Load all parameters (using correct slots for wide types) + int slot = 0; + for (int i = 0; i < info.descriptor().parameterCount(); i++) { + final TypeKind typeKind = TypeKind.fromDescriptor(info.descriptor().parameterType(i).descriptorString()); + cb.loadLocal(typeKind, slot); + slot += typeKind.slotSize(); + } + // Invoke the updated method + switch (kind) { + case VIRTUAL -> cb.invokevirtual(info.owner(), info.name(), callDesc); + case INTERFACE_VIRTUAL -> cb.invokeinterface(info.owner(), info.name(), callDesc); + default -> cb.invokestatic(info.owner(), info.name(), callDesc); + } + // Apply the converter + cb.invokestatic(this.converterOwner, this.converterMethod, converterDesc); + cb.areturn(); + } + ); + + // 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[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, context.currentClass(), bridgeName, info.descriptor()); + 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..ee97adc --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/BridgeMethodRegistry.java @@ -0,0 +1,62 @@ +package io.papermc.classfile.method.transform; + +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassFile; +import java.lang.classfile.CodeBuilder; +import java.lang.constant.MethodTypeDesc; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * 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. + * + * @param baseName suggested name for the bridge method + * @param descriptor method descriptor (parameters and return type) + * @param body code generator for the method body + * @return the actual name assigned to the bridge method + */ + public String registerBridge(final String baseName, final MethodTypeDesc descriptor, final Consumer body) { + String name = baseName; + int counter = 0; + while (this.bridges.containsKey(name)) { + name = baseName + "$" + (++counter); + } + this.bridges.put(name, new MethodGen(descriptor, body)); + 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 index ed43038..4b7ba22 100644 --- 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 @@ -3,6 +3,7 @@ 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; @@ -27,10 +28,12 @@ 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) { + public ConstructorAwareCodeTransform(final MethodRewriteIndex index, final CodeTransform fallbackTransform, final TransformContext context) { this.index = index; this.fallbackTransform = fallbackTransform; + this.context = context; } @Override @@ -50,7 +53,7 @@ public void accept(final CodeBuilder builder, final CodeElement element) { // end of a constructor level final InvokeInstruction invoke = (InvokeInstruction) element; final Level level = this.bufferStack.pop(); - final MethodTransforms.BoundRewrite boundRewrite = MethodTransforms.setupRewrite(invoke); + 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); 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..7bd4f04 --- /dev/null +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/transform/MethodTransformContext.java @@ -0,0 +1,81 @@ +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.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)); + } + + /** + * 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) { + } + + /** + * 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 index fac2e17..a002a95 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -8,6 +9,7 @@ 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; @@ -36,14 +38,16 @@ static void writeFromCandidates(final List candidates, final Cons } } - static @Nullable BoundRewrite setupRewrite(final CodeElement element) { + static @Nullable BoundRewrite setupRewrite(final CodeElement element, final TransformContext context) { final ClassDesc owner; final String methodName; + final MethodTypeDesc descriptor; final Writer rewriter; if (element instanceof final InvokeInstruction invoke) { owner = invoke.owner().asSymbol(); methodName = invoke.name().stringValue(); - rewriter = (emit, poolBuilder, rewrite) -> rewrite.transformInvoke(emit, poolBuilder, owner, methodName, invoke); + descriptor = invoke.typeSymbol(); + 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(); @@ -56,22 +60,29 @@ static void writeFromCandidates(final List candidates, final Cons } owner = methodHandle.owner(); methodName = methodHandle.methodName(); - rewriter = (emit, poolBuilder, rewrite) -> rewrite.transformInvokeDynamic(emit, poolBuilder, bootstrapMethod, methodHandle, args, invokeDynamic); + // for VIRTUAL, VIRTUAL_INTERFACE, this descriptor has the receiver type as the first param. + // we remove it later just for purposes of descriptor matching, method actions are expected to handle it accordingly + descriptor = methodHandle.invocationType(); + rewriter = (methodContext, rewrite) -> rewrite.transformInvokeDynamic(methodContext, bootstrapMethod, methodHandle, args, invokeDynamic); } else { return null; } - return new BoundRewrite(rewriter, owner, methodName); + final MethodTransformContext.MethodInfo info = new MethodTransformContext.MethodInfo(owner, methodName, descriptor); + return new BoundRewrite(rewriter, info, context); } - record BoundRewrite(Writer writer, ClassDesc owner, String methodName) { + record BoundRewrite(Writer writer, MethodTransformContext.MethodInfo methodInfo, TransformContext context) { public boolean tryWrite(final Consumer emit, final ConstantPoolBuilder poolBuilder, final MethodRewrite methodRewrite) { - return this.writer.write(emit, poolBuilder, 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(Consumer emit, ConstantPoolBuilder poolBuilder, MethodRewrite rewrite); + 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 index 7605378..d97f4da 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -9,8 +10,6 @@ import java.lang.classfile.instruction.InvokeInstruction; import java.util.List; -// won't work if we are rewriting any constructors that aren't lambdas - /** * This is a transform for rewriting all non-INVOKESPECIAL instructions. * Method constructors that aren't lambdas require iterating over the full @@ -20,19 +19,21 @@ public class SimpleMethodBodyTransform implements CodeTransform { private final MethodRewriteIndex index; + private final TransformContext context; - public SimpleMethodBodyTransform(final MethodRewriteIndex index) { + 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); + final MethodTransforms.BoundRewrite boundRewrite = MethodTransforms.setupRewrite(element, this.context); if (boundRewrite == null) { builder.with(element); return; } - final List candidates = this.index.candidates(boundRewrite.owner(), boundRewrite.methodName()); + 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, 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/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/test/java/io/papermc/classfile/SimpleTest.java b/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java deleted file mode 100644 index 2862e74..0000000 --- a/classfile-utils/src/test/java/io/papermc/classfile/SimpleTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.papermc.classfile; - -import data.methods.Methods; -import data.methods.Redirects; -import data.types.hierarchy.Entity; -import data.types.hierarchy.Player; -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 io.papermc.classfile.method.action.DirectStaticCall; -import java.lang.constant.ClassDesc; -import java.util.ArrayList; -import java.util.List; - -import static io.papermc.classfile.ClassFiles.desc; - -class SimpleTest { - - 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))); - } - - 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/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/action/TestDirectStaticCall.java b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestDirectStaticCall.java index b6a0d60..27d6542 100644 --- 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 @@ -31,6 +31,10 @@ void test(final TransformerCheck check) { 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); 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..3362e81 --- /dev/null +++ b/classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSubtypeReturn.java @@ -0,0 +1,30 @@ +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; + +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, MethodNamePredicate.exact("get", "getStatic"), MethodDescriptorPredicate.hasReturn(ENTITY), new SubtypeReturn(PLAYER)) + ); + 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 index a8ccc0d..5ba4e70 100644 --- 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 @@ -1,45 +1,47 @@ package io.papermc.classfile.method.transform; import io.papermc.classfile.ClassFiles; -import java.lang.classfile.CodeElement; 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; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; class TestConstructorAwareCodeTransform { @Test void nonInvokeInstructionReturnsFalse() { - final CodeElement element = mock(NewObjectInstruction.class); + final NewObjectInstruction element = NewObjectInstruction.of(ConstantPoolBuilder.of().classEntry(ClassDesc.of("java.lang.Object"))); assertThat(ConstructorAwareCodeTransform.isConstructor(element)).isFalse(); } @Test void invokespecialInitReturnsTrue() { - final InvokeInstruction invoke = mock(InvokeInstruction.class, RETURNS_DEEP_STUBS); - when(invoke.opcode()).thenReturn(Opcode.INVOKESPECIAL); - when(invoke.method().name().equalsString(ClassFiles.CONSTRUCTOR_METHOD_NAME)).thenReturn(true); + 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 InvokeInstruction invoke = mock(InvokeInstruction.class, RETURNS_DEEP_STUBS); - when(invoke.opcode()).thenReturn(Opcode.INVOKESPECIAL); - when(invoke.method().name().equalsString(ClassFiles.CONSTRUCTOR_METHOD_NAME)).thenReturn(false); + 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 InvokeInstruction invoke = mock(InvokeInstruction.class, RETURNS_DEEP_STUBS); - when(invoke.opcode()).thenReturn(Opcode.INVOKEVIRTUAL); + 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/methods/statics/PlainUser.java b/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java index e356d02..3ce3cc5 100644 --- a/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java +++ b/classfile-utils/src/testData/java/data/methods/statics/PlainUser.java @@ -2,6 +2,7 @@ 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; @@ -12,7 +13,8 @@ final class PlainUser { public static void entry() { final Player player = new Player(); - final Entity entity = new Player(); + final Entity entity = new Mob(); + final Entity entity2 = new Mob(); player.addEntity(entity); Player.addEntityStatic(player); @@ -38,5 +40,23 @@ public static void entry() { 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/types/hierarchy/Entity.java b/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java index 32f2fd1..d83d4ba 100644 --- a/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Entity.java @@ -1,6 +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 index 57533d9..ca76214 100644 --- a/classfile-utils/src/testData/java/data/types/hierarchy/Mob.java +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Mob.java @@ -1,9 +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 index 4ce766d..ccff3f8 100644 --- a/classfile-utils/src/testData/java/data/types/hierarchy/Player.java +++ b/classfile-utils/src/testData/java/data/types/hierarchy/Player.java @@ -1,8 +1,12 @@ 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"; @@ -28,4 +32,19 @@ public static void addEntityAndPlayerStatic(final Player player, final Entity en 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/resources/expected/data/methods/inplace/SubTypeReturnUser.class b/classfile-utils/src/testData/resources/expected/data/methods/inplace/SubTypeReturnUser.class new file mode 100644 index 0000000000000000000000000000000000000000..9cdea63d41cdc31ab0299d90b3b56be38327ef1c GIT binary patch literal 2170 zcmbVNZFAd15Psx0C5}=%ZfY7z(o)*u7+OQy0F6y34GAfAoDioep>Le+OQKehj4XG? zKZ`H44Koa%8GaMPbCT@5*f9gv_;k0o&+fCkcdI}DKKLEL5_Xb^AgUu~AdZBF^b7Gy z6f9xy7S?JnWZlz{D4DkDEog}5X18>VXqapWPZaj0x92okh1KAlgpQ~h#tax3r~0lW z`eJUj?5lg7rfe1VOz8@@zSk)%*`DcjiVBf3kXDFMBC4M7%z6?ToYgUD;2g3VCVW{QL?~UD?{KGW*FDp* z3)ObBX_0<$3>Wa8j%j6NM#F_ea(%kY731%trQ;HDL(-CghRX&%z!eQ~LBE|KlLK5W zhx|zE8a~vKQ!wUi5Us9mhDFaS`X8NYwim6A>&)%3iko)R5_QRp)HYew4e7OA`(aDE z8e)r1LuyEsOz8=Ah|o6RZ0$1=eLhS?|$ z^IjSn!~%V741#yLgz%FG{a=kHt4`ak%Vks5Fxyw~8!BJy8uuK>Yk96{hRq<2nN%D} z-s&{6X?!>AstvgY#Z&@k_+L$;f;AoM2ENAzug2kxGVNE+OIaw3{aQoNyVwzR&rwHY zjKLnR{yN$nLQp_Mg@l&&A{bw?9sLnx?L2O zRW&`yVjK5uTe^#uXtiWZ$3qQQ`*JcUJROfT%nVoM)d_MjqJ5_+`rDQ+&*(rG63iMt-X}-r%ahx5jro{~Fpa z{`MyvjX+4)O64H>%T$YevQ!w`Csw4{02`~^V@GQ@#p|Q0hQ}- z@veqHaB)6%fI2WApNi+-;%dZ~B@Q4d)2H;F+KA#2h2rsq_D8US+nD2O9EE3wIlM{txSPKPvzL literal 0 HcmV?d00001 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 index ea3e3ada1413bc7439dbd27f753c2f3ee108e888..2c038c7baaa7f85f1d5bcaaefd5982761012a142 100644 GIT binary patch literal 4300 zcmcIn*>@9F82{aLnKqfWVJO(hB838N3I=h-6gaF0qzh7P1zeaW109-9YBDL-9d}Vd zKwMGWcTf~)DTwHs9*>Xy13voZtLJ$9-OOZ~LQ*_E$DEUyd%ydwzwh4fe)G#eqZa|J z!%u$rP@(_SG^(%96cph93qT(|XzJv3mX zBfX}P(o?bCp~&WhK4hf)P^7+qKwUwuwq%bVew0YQY85pI3RLRx_&Uor?ID3VjjaXc zob_l^mlV`gOqGH)o?xe~+h$C`G=Z5}`F_LhO~%uaHs{^Yoze#eq{dt zO)0om;F>W+I_;Ec?OvTR6LG1@hnXs7VYWbpopja&Y8#tcFPDh=5yBj4wO(LaKq%Y{z4P>u9_ndTvM>a~@XFj73bV9*Y?R%x`_p z^@u1~A|-DSn3avM7`MQ}{0N<06wQm-rO23X z#7zorR&fh%6__%n)?rBtRHuvwGG@wXPg?DnL_%Oe0p0n4yUH~PER*iuF3?hBN9EG& zxHdkVywF>)T)_&dYbAI7n2#P%Io``s>xHRtqpXths|8j}!k4^6Xehy2scM~*>j}Jx zYA8imSRIr5MvvDC8V(0Gn~Af-NdK<$)?( z7jh@NB5l|mdo3d+rS6gI?Oz1m3jS%ug= z^cbBwHEoEFiX+aCm@npt-gW^?ZloPbxWJsAl zJ|nz&+^p;<6xbLJEfXSNc+o|B1)Xmxq&){)5#4#>+Y5>3uqh($#2p>K%PzabqLcN_ zRW8LR2b>$+Re}2-wCDNm?@VS=F=M?c4{J3SoD+S$tZHmwRwt8o+U5pzVGH7hnle-c zu}@(6#07Z5z?!;(SQ$i+%AS#3$g?UE*jH1IDg3`6Df?CQV_y)r)>L465C^Vm&%Lg5 z)#UO#*Hoe-h&sNu263dgw*QhA>HX13s&KJy>SCa;`|$?eRPdIHw{c8hZuTz8c8sPq znCvqmt$Ke?TxTr$Eo=9|>w!x#MZnSu-tx4$Vw2@YDoK4`ktH;eMtESt8D)>NP z{uQ_tfgD*5xVXY+Z=WIkn zHNqU6&=?7Onob;%Yu}S}QA1E(|5Fgg65PXnOaA1O-!$w5@8c-Kw~&h!RO%-e8K;T?(+z3 zb2&LZiy6bXj>Ot+;u30yG50LyOSVyb4YV%~H=o1e5`?#nU@2+e0JoQhN=c0RJmMJ= z%R*%?>nMgfFAtSFOrsd(R0%2G@&(Q-LKWe2*yKZZGv6IPG%p&##b(z^7da|Jm0Y2f z9Ptu~{*ZrBmN<$ZISqsY&79M}QS9Jc4XILHb}rYeLRId%#IyuE`8+_ZDnu>fFgObe z6}zyTsMd*6^m625?tW$xP4f5RH1?p6_&$o`+y*97_A(Cg^RAERAHx$Ys3t4xR~UeW zBs;v{(9Urm^YaIm;Q@~P_=|~8kuM;8xE5)SR3Z0nCr4GV>BRsyv12vkSsnDOW<0Bd zp4E(Jb+2djLC@--XLZQ4`jBUJh*srZdKUG3)=-|;2DfJ&G=4BnC3PAWU^=3tw9(^E z%pkh6h{kMsI)_-)Q+6JaXuwf&9^OMBX*J?GJ$cWE!;CHm?}9K<>G7TRXlFgzInTy< M&&Chl>JK>lF9X}b=>Px# delta 1273 zcmX|=?^Bdj6vsbzpWWwyXCK&yMaYF^0mXm?L4>gsqOln=1FS$nM1xI|)(8n!3d_i} zGBFiyzq6*A-Zc55#zqiloP1eR(|^!Re?k92)48zoytqHk_k8d7oO{l_f7Je!aDV@A z{XWpc*URn^*To^~;Zd}FwLKN{@F`NnG7}R$)46PJVKA4;WluW96-oV#N*246u*D-n znWCma|Kn6e8iyPn(v2!*Cp;>s)cvZirJO3mN-4@3`c7x&Gp$pZ>B-i?+-!Dwva@l> z=+qcSt)jL;f9s^YO>nzM9Xs@IYPVhQ(LkeCq1NIinjLl;yG2o@{ZN%24K+u1(dMw* zqn!@D5*o1g7{()dIb5M{SV>)DMT_^c&!N-cT}pB*vMTjqE9rI{bogD9%>_55 z|BTl8@0&(vT`u#1nPOUb73Ei%H^zq&@JbvTxv|3@-R6xQ7-hw{w75QsQ$V+Dmf{q$ z-@vnCeICc9UN+a_MCDEGqK}ZgE;iX9e@bLhAV(1F}9N!}yVtg$!(K{hqU1C30eF&=1S0*&!NvMXVc3)0kgxWs#+_9ExuoZS93A)jKBmo|2%W%qI(n+mWq0XCDz=E!k2&$}M* Tt_Qr21MJfP`yx1dle7N=?N!;d 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 0000000000000000000000000000000000000000..0346cf2b4d7a53656df575ec9b55a045784ff285 GIT binary patch literal 2397 zcmbVNZBrXn6n-uwSsJzw!b?jLT3SS23ftDfYJ*sztso&qLTT}(x+E7^*zAVg4G!b@ zNq>;~p`|$E_}TF{IX-tc(4@t{_+j_nJ^P%u=bn4cU;iBa0pKD0B%08yA*Lgagg|P? z+&2x|bheDg8#}TP2qdO0#|mZynny<0l4!vN4M`oX&;`1RW?&j68Em^n-&l%P0ewpb zS+|f6%)lxLygM?Q4S~TyS^CDdB|XzCY#$i5TQGGnTD*WBT+(n^M=!3_rItVN16g85ZiNYUXTzi|*I4!}C&&k$G)oNU z9rSA$&~X*l1UgT_2xw)cVLLQ35?QICE)&t!tT?-&V@MH}h?q%}{9zsMVT3_YByg1+ zg)Svcf8%+`hZfUr!;||^z=y+`* zI%!{^&69f-%ab|R$yIDyVEDAG-cB?PfhiT)2V6&@vwou}GL6&I@S(s&T{Zb(_4tM- zgNo<)#!9$8V0eY#i7!2Y*qmFG0_|DLk-17~Lwc*`hD}LY6_veadY1aG3S+^x#T2t(9!;q4Bx8fD#BTJFk{U+g)s$zJLW?eV%1J5j1b4g*MJpnBRy*-ZW+dZpvviV+yzU*~DHA#Q1PRveA9SoxxQo$=FMXU%6>QhHpI*Vj3Uw z>mDDa05vl)bF3^@h}J0m`4Hb8LN1QIz(qdk-_Xsa?{~DUzrB^HIOK z^c}$laucsGD)1-T?!|gz1gMunsI_l={ScE+kKi$ecyIg!n&?d^C|@CzYoCg}IgEXt zF=_Y_pWr@a1K7l;m}T_Cc#b*BRE7(zoCm}|Vm1#ci$QWa4pA1z9y%$%&n!Qqe$NSh zp7;ngPn6xFTq>-;yTtqvEcMnqZ*+Fk<4!TZE5;H|<2A62{G8Q?*mvIHpIF%*( z>1_b}^!)>_;U^5HlBz}+tvZ~}kaIwb`2B?<5Wv%|0=l?{$3nUl#UX-WE)dv+&+(LN hl89&c5?|BtBEQF&>@qP~EFo9jJwDm(p6vFJ`xj41c4Ytn literal 0 HcmV?d00001 diff --git a/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java b/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java index df234bd..9962981 100644 --- a/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java +++ b/classfile-utils/src/testDataNewTargets/java/data/methods/Redirects.java @@ -53,6 +53,10 @@ public static Position toPositionFuzzy(final Object 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/hierarchy/Entity.java b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java index 32f2fd1..70656c4 100644 --- a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Entity.java @@ -1,6 +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 index 24ea6f1..302a18e 100644 --- a/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java +++ b/classfile-utils/src/testDataNewTargets/java/data/types/hierarchy/Player.java @@ -1,6 +1,8 @@ package data.types.hierarchy; -@SuppressWarnings("unused") +import org.jspecify.annotations.Nullable; + +@SuppressWarnings({"unused", "DataFlowIssue"}) public class Player implements Entity { public Player() { @@ -38,4 +40,31 @@ public static void addEntityAndPlayerStatic(final Player player, final Entity en 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; + } } From e9ea98da1f22cc12d7ce741010c40be12b263cb0 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sun, 1 Mar 2026 21:43:12 -0800 Subject: [PATCH 4/4] Add some more conversion types --- .../java/io/papermc/classfile/ClassFiles.java | 73 ++++++++++++ .../generation/ParameterGeneration.java | 69 +++++++++++ .../method/MethodDescriptorPredicate.java | 4 +- .../classfile/method/MethodRewrite.java | 20 ++-- .../method/action/DirectStaticCall.java | 37 +++--- .../method/action/MethodRewriteAction.java | 3 +- .../method/action/SubtypeReturn.java | 11 +- .../method/action/SupertypeParam.java | 52 +++++++++ .../method/action/WrapParamValue.java | 110 ++++++++++++++++++ .../method/action/WrapReturnValue.java | 76 ++++++------ .../transform/BridgeMethodRegistry.java | 28 ++++- .../transform/MethodTransformContext.java | 19 ++- .../method/transform/MethodTransforms.java | 10 +- .../method/action/TestSubtypeReturn.java | 4 +- .../method/action/TestSupertypeParam.java | 34 ++++++ .../method/action/TestWrapParamValue.java | 36 ++++++ .../methods/inplace/SuperTypeParamUser.class | Bin 0 -> 2129 bytes .../statics/param/ParamDirectUser.class | Bin 0 -> 3429 bytes .../statics/param/ParamFuzzyUser.class | Bin 0 -> 3789 bytes .../statics/returns/ReturnDirectUser.class | Bin 2397 -> 2528 bytes 20 files changed, 492 insertions(+), 94 deletions(-) create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/generation/ParameterGeneration.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/action/SupertypeParam.java create mode 100644 classfile-utils/src/main/java/io/papermc/classfile/method/action/WrapParamValue.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/action/TestSupertypeParam.java create mode 100644 classfile-utils/src/test/java/io/papermc/classfile/method/action/TestWrapParamValue.java create mode 100644 classfile-utils/src/testData/resources/expected/data/methods/inplace/SuperTypeParamUser.class create mode 100644 classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamDirectUser.class create mode 100644 classfile-utils/src/testData/resources/expected/data/methods/statics/param/ParamFuzzyUser.class diff --git a/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java index 0e9e033..51fcc25 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/ClassFiles.java @@ -1,6 +1,11 @@ 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; @@ -13,14 +18,45 @@ public final class ClassFiles { 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))) { @@ -29,4 +65,41 @@ public static MethodTypeDesc replaceParameters(MethodTypeDesc descriptor, final } 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/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 index 44b4323..00146a9 100644 --- a/classfile-utils/src/main/java/io/papermc/classfile/method/MethodDescriptorPredicate.java +++ b/classfile-utils/src/main/java/io/papermc/classfile/method/MethodDescriptorPredicate.java @@ -9,14 +9,14 @@ public sealed interface MethodDescriptorPredicate extends Predicate { + throw new IllegalArgumentException(s); + }); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean doesMatch(final String name, final MethodTypeDesc descriptor) { - return this.methodName.test(name) && this.descriptor.test(descriptor); + 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().name(), context.methodInfo().descriptor())) { + if (!this.doesMatch(context.methodInfo())) { return false; } try (TrackingConsumer.Instance _ = context.prime()) { @@ -47,14 +48,7 @@ public boolean transformInvokeDynamic( final InvokeDynamicInstruction invokeDynamic ) { // owner validated by caller - - // For VIRTUAL/INTERFACE_VIRTUAL, the descriptor includes the receiver as this first param. - // we need to remove that so that the descriptor matches a non-dynamic invocation, which is what our API expects for matching - final MethodTypeDesc descriptorForMatching = switch (methodHandle.kind()) { - case VIRTUAL, INTERFACE_VIRTUAL -> methodHandle.invocationType().dropParameterTypes(0, 1); - default -> methodHandle.invocationType(); - }; - if (!this.doesMatch(context.methodInfo().name(), descriptorForMatching)) { + if (!this.doesMatch(context.methodInfo())) { return false; } final MethodRewriteAction.BootstrapInfo info = new MethodRewriteAction.BootstrapInfo(bootstrapMethod, invokeDynamic.name().stringValue(), invokeDynamic.typeSymbol(), args); 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 index daa2e57..d1f8712 100644 --- 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 @@ -12,9 +12,14 @@ 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. @@ -27,7 +32,6 @@ */ public record DirectStaticCall(ClassDesc newOwner, @Nullable String staticMethodName) implements MethodRewriteAction { - private static final String DEFAULT_CTOR_METHOD_PREFIX = "create"; public DirectStaticCall(final ClassDesc newOwner) { this(newOwner, null); @@ -39,12 +43,7 @@ public Optional isValidFor(final MethodNamePredicate namePredicate, fina } private String constructorStaticMethodName(final ClassDesc owner) { - if (this.staticMethodName != null) { - return this.staticMethodName; - } - // 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); + return Objects.requireNonNullElseGet(this.staticMethodName, () -> constructorMethodName(owner)); } private String staticMethodName(final String originalName) { @@ -59,21 +58,18 @@ public void rewriteInvoke(final MethodTransformContext context, final Opcode opc final MethodTypeDesc descriptor = context.methodInfo().descriptor(); final ClassDesc owner = context.methodInfo().owner(); final String name = context.methodInfo().name(); - MethodTypeDesc newDescriptor = descriptor; - if (opcode == Opcode.INVOKEVIRTUAL || opcode == Opcode.INVOKEINTERFACE) { - newDescriptor = descriptor.insertParameterTypes(0, owner); - } else if (opcode == Opcode.INVOKESPECIAL) { - if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { - newDescriptor = newDescriptor.changeReturnType(owner); - context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.newOwner(), this.constructorStaticMethodName(owner), newDescriptor))); - return; + 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 if (opcode != Opcode.INVOKESTATIC) { - 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(), this.staticMethodName(name), newDescriptor))); + context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.newOwner(), newMethodName, newDescriptor))); } @Override @@ -81,13 +77,12 @@ public void rewriteInvokeDynamic(final MethodTransformContext context, final Dir final MethodTypeDesc descriptor = context.methodInfo().descriptor(); final ClassDesc owner = context.methodInfo().owner(); final String name = context.methodInfo().name(); - MethodTypeDesc newDescriptor = descriptor; + 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.SPECIAL || kind == DirectMethodHandleDesc.Kind.INTERFACE_SPECIAL || kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { + } else if (kind == DirectMethodHandleDesc.Kind.CONSTRUCTOR) { if (ClassFiles.CONSTRUCTOR_METHOD_NAME.equals(name)) { - newDescriptor = newDescriptor.changeReturnType(owner); 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; 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 index e28807d..4c55d4b 100644 --- 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 @@ -2,7 +2,6 @@ import io.papermc.classfile.method.MethodDescriptorPredicate; import io.papermc.classfile.method.MethodNamePredicate; -import io.papermc.classfile.method.MethodRewrite; import io.papermc.classfile.method.transform.MethodTransformContext; import java.lang.classfile.Opcode; import java.lang.constant.ConstantDesc; @@ -12,7 +11,7 @@ import java.util.List; import java.util.Optional; -public sealed interface MethodRewriteAction permits DirectStaticCall, SubtypeReturn, WrapReturnValue { +public sealed interface MethodRewriteAction permits DirectStaticCall, SubtypeReturn, SupertypeParam, WrapParamValue, WrapReturnValue { /** * Check that the specified rewrite is configured correctly for this action. 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 index 086a168..a364ed7 100644 --- 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 @@ -26,7 +26,7 @@ public record SubtypeReturn(ClassDesc newReturnType) implements MethodRewriteAct @Override public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { // only valid if you are search by return type - if (!(descriptorPredicate instanceof MethodDescriptorPredicate.ReturnType)) { + if (!(descriptorPredicate instanceof MethodDescriptorPredicate.HasReturn)) { return Optional.of("You must use a return descriptor predicate on " + descriptorPredicate); } return Optional.empty(); @@ -43,14 +43,9 @@ public void rewriteInvoke(final MethodTransformContext context, final Opcode opc 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]); - // For VIRTUAL/INTERFACE_VIRTUAL kinds, descriptor (i.e., methodHandle.invocationType()) includes the receiver - // as the first parameter. But MethodHandleDesc.ofMethod for VIRTUAL adds the receiver itself, - // so we must strip it before constructing the new handle. - final MethodTypeDesc handleMethodType = switch (kind) { - case VIRTUAL, INTERFACE_VIRTUAL -> info.descriptor().dropParameterTypes(0, 1).changeReturnType(this.newReturnType); - default -> info.descriptor().changeReturnType(this.newReturnType); - }; + 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); } 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 index 1a82fed..e38c817 100644 --- 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 @@ -1,20 +1,21 @@ package io.papermc.classfile.method.action; -import io.papermc.classfile.ClassFiles; +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.TypeKind; 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.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 @@ -35,70 +36,67 @@ public record WrapReturnValue(ClassDesc converterOwner, String converterMethod, @Override public Optional isValidFor(final MethodNamePredicate namePredicate, final MethodDescriptorPredicate descriptorPredicate) { // only valid if you are search by return type - if (!(descriptorPredicate instanceof MethodDescriptorPredicate.ReturnType)) { + if (!(descriptorPredicate instanceof MethodDescriptorPredicate.HasReturn)) { return Optional.of("You must use a return descriptor predicate on " + descriptorPredicate); } - // TODO maybe this works??? - // if (namePredicate instanceof MethodNamePredicate.Constructor) { - // return Optional.of("Cannot wrap return value of constructor"); - // } + 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(); - // Emit the original call with the new return type final MethodTypeDesc callDesc = info.descriptor().changeReturnType(this.newReturnType); - context.emitChangedDescriptor(opcode, callDesc); - // Emit the converter: converterOwner.converterMethod(newReturnType) -> original return type + 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); - // TODO might want to, instead, change just the call to a generated method that handles this to avoid an extra call - context.emit(InvokeInstruction.of(Opcode.INVOKESTATIC, context.constantPool().methodRefEntry(this.converterOwner, this.converterMethod, converterDesc))); + + 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(); - // descriptor = invocationType of the method handle - // For VIRTUAL/INTERFACE_VIRTUAL, descriptor includes the receiver as the first param. - // The call descriptor strips the receiver and uses the newReturnType. - final MethodTypeDesc callDesc = switch (kind) { - case VIRTUAL, INTERFACE_VIRTUAL -> info.descriptor().dropParameterTypes(0, 1).changeReturnType(this.newReturnType); - default -> info.descriptor().changeReturnType(this.newReturnType); - }; + 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.name() + "$" + this.converterMethod, - info.descriptor(), + info.owner(), + info.name(), // don't need to handle ctor names, not allowed + staticReplacement, + ParameterGeneration.standard(), cb -> { - // Load all parameters (using correct slots for wide types) - int slot = 0; - for (int i = 0; i < info.descriptor().parameterCount(); i++) { - final TypeKind typeKind = TypeKind.fromDescriptor(info.descriptor().parameterType(i).descriptorString()); - cb.loadLocal(typeKind, slot); - slot += typeKind.slotSize(); - } // Invoke the updated method - switch (kind) { - case VIRTUAL -> cb.invokevirtual(info.owner(), info.name(), callDesc); - case INTERFACE_VIRTUAL -> cb.invokeinterface(info.owner(), info.name(), callDesc); - default -> cb.invokestatic(info.owner(), info.name(), callDesc); - } + emitInvoke(cb, kind, info, callDesc, false); // Apply the converter - cb.invokestatic(this.converterOwner, this.converterMethod, converterDesc); - cb.areturn(); + 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[ClassFiles.BOOTSTRAP_HANDLE_IDX] = MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.STATIC, context.currentClass(), bridgeName, info.descriptor()); + 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 index ee97adc..8374f85 100644 --- 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 @@ -1,13 +1,19 @@ 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) @@ -23,18 +29,34 @@ public final class BridgeMethodRegistry { * 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. * - * @param baseName suggested name for the bridge method + *

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 String baseName, final MethodTypeDesc descriptor, final Consumer body) { + 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); } - this.bridges.put(name, new MethodGen(descriptor, body)); + 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; } 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 index 7bd4f04..1d6cdbf 100644 --- 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 @@ -8,6 +8,8 @@ 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; @@ -30,6 +32,21 @@ default void emitChangedDescriptor(final Opcode opcode, final MethodTypeDesc new 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. * @@ -68,7 +85,7 @@ default void emitChangedDescriptor(final Opcode opcode, final MethodTypeDesc new * This is just for method matching, combining method information from both * INVOKE* and INVOKEDYNAMIC instructions. */ - record MethodInfo(ClassDesc owner, String name, MethodTypeDesc descriptor) { + record MethodInfo(ClassDesc owner, String name, MethodTypeDesc descriptor, boolean isInterface) { } /** 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 index a002a95..f77cc1d 100644 --- 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 @@ -42,11 +42,13 @@ static void writeFromCandidates(final List candidates, final Cons 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(); @@ -60,14 +62,14 @@ static void writeFromCandidates(final List candidates, final Cons } owner = methodHandle.owner(); methodName = methodHandle.methodName(); - // for VIRTUAL, VIRTUAL_INTERFACE, this descriptor has the receiver type as the first param. - // we remove it later just for purposes of descriptor matching, method actions are expected to handle it accordingly - descriptor = methodHandle.invocationType(); + // 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); + final MethodTransformContext.MethodInfo info = new MethodTransformContext.MethodInfo(owner, methodName, descriptor, isInterface); return new BoundRewrite(rewriter, info, context); } 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 index 3362e81..a744776 100644 --- 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 @@ -13,6 +13,8 @@ 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 { @@ -23,7 +25,7 @@ class TestSubtypeReturn { @TransformerTest("data.methods.inplace.SubTypeReturnUser") void test(final TransformerCheck check) { final List rewrites = List.of( - new MethodRewrite(METHODS, MethodNamePredicate.exact("get", "getStatic"), MethodDescriptorPredicate.hasReturn(ENTITY), new SubtypeReturn(PLAYER)) + 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/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 0000000000000000000000000000000000000000..c0fe4d931b91e290759f8d0b1747b258e2978913 GIT binary patch literal 2129 zcmbVN>vGdZ6#mwBqM$gj;)JA38VH#Hwi{5Ph0@whOWbfNj!EmpX`s-q<%I}j$<>7! zKUV(~2s3?v4iD2O==7{5->_q*wa4Dwv)^~_I{NFM<39mxqoyK;xPn9y6PRSsp7WPn zx48XGe^7fa44+}LVA`g?$q>(Es|uzV7V6yRdPDd}PTkXY!c~PL6-_fNx3B$HQ+WE3 zDO~OvM=gEV;w|B-P!X5V*(6fX7!<>?y`Ui&Rx_nB#C*0YY3U^9C2c06RebK7hKkcz zRIrr9M>xYU*A61^O)Cn*W0-ZtZ-MEGvSXJ6%VM~gDZL+SK07Q*Q<|2O_!ujc32qpo zNh!G=x%yrTI{dcfr-5zwreo{HFhDmyjdNI4a9(O#qcrur9Ktf39_3y&4Tg2ezrb)l z6S+HRZp-X5cnKK=SxLw-tc)<&Lo-|&VRASjS#lzV4XN`o!@1E;bX`!Olj4DXG3}noe*ZRI65;z%|5eQ|5kYxKdYSE77#ravb0DUEYi| zYgkK7U`B(cA*bOs!;KRjm9Z{ltEoxM(eIv$?{Qy2DTy7F8P91Le#8FzHns(yJUB(JRw$T4N-+I>gz75K({io9KB9u#8a3d?krZI z5o?aD%#-E?dUzH|?-CxbH$Q&;!ROi0j}B3-O`ot(4JzUS_9Ie-1L@dtpn@KX>0)G4S}(SSyQ z@Lg?Hi)S@+Ies;DS5LbFjl+g%xFZ5}{R49fngkAHG*^qeYkA#?uNbaf1$za$t7VMN+gd)a+d*`oOA_9<-Gp-y7O-1I z4|)X}2$EeB=<6Ry7AshAjck0WV5Vg?;^Y2HB7|3PK*2#7c1U1<4c{Vet4?dws2~=@Q3ZoC;Fv&96=G#?fph)dGAnsJZ>d5y zh&WCtIH}^4EPzM5SS(L&j=*kPzh5wHecCdo3)yUh0zEBhoe?ZXp(BXDRNLKhGzUW)>XpG2TzYf`_N2p>qD zq~8Ni;sU)@{YouB|DKxSm3XHF_Ec=NuQi7DRSPXkrby;Bfl#?A2^C14W}lk+1HTL9 zQnu8VM~q+Jo7M13@*Unr;i|A6A*)<9+%j(;xwN09((XLpooe}BY8_iN>=WADL%F2j zj8(AH`nVy*+V+gDkIT;HIm?}M9hb`R5yCjs(g-ySDvRB{jT5&@LBlxK(ttf-yeBaI zf?nMrbW0OD!yxmq?UAclC9X6bLrKn-)%x~IpJS}v8*SRQRjOS_hA z(@?JR>+i}#&=2yeIiZ=EtnNgUmUXX?PgGQD#$Nz>Hrv8?7eI!`ji?@0w&fy2*Lr`kdlm^@@^yK)}zsv>Ycukk-`G2s%m zdP!$`&DAqexp%i&YB?I*_7XYK^1E zEsPbsiBU4{#2t*`B3IRmMb7M_^p9c|VtUVcq*?aOGzG;Kkcyspv~!Zl`$ZK7Q#nul?N7+&OcMtc4;^q18f!fxIr=Ursb zLY`sfh>%+wv$m7ZUZQj|>po)aXWkwn^ith@;h^kZu1#)V8=rDtnpop6pWvIH`3`!p zxqOdyu3c_?bL=Qxe#3Vq7dDFT(#fJo5cMfK79`L*e*8rO(LT$205@UqDaXkU628mj z++(pVbYYJ68!S2nih82`O#+HJSnc1gKH!E%(+79^CTAVGwpg+eJvp|$nEO|sC1BpZ?q*m|NM z;(;e#=tsvdjH9DN3!Pz{(NB*0CprG!>@GWxj-{U!c&u^dSdEfl?-<`Vv_ToDg z0aPdmYN$k&fPTg}Z$y$t`gG(-?2H+A1gdsfY0KFyP|?;tsGwS4Rl;zLh%=TkbCFTY z%o^GF=vX9a$0L1q+;A*AtwMn+P1aJst+%(geNaUm>J>C-(6K;ZVZlbqbVlt&E^^p? z(|_Df=kh7@sGS>dXeuty+SWItKZ;q>U$lKhL8HLhIVF2jnWTykREeiTK9*=$DiNtg z*9w*iG?zozLXSgPBa<<+D#BPUV_Y#`jDsO^-mGCIRtZ$nNOFvB+S>aHq~{$g899|t z$7Mi~9&Zq>#Tv9ISS!_91)64&E#SES14cdbCEqlvpiN*wQL@{%lctdlL7+qWS&r0alb2*wN;OS&HMql`>#etixsvTd-9?i@8V#jbz>|WaNm7 zE^Jq@L&Fo2A2+H3mu>>*1nRTqxxAG%`|WgpKA8+L<)aeyE`e=ca^07RK4~+2lMpJ< ztl%ku_Gw9yE2L~Lk}YlY4&2(k2V3 zoV8*G7SL=dX!2R>6+1O)ClF(9(IlU0+PvTuxQuJshhf9>KF*19zWmDAH8bt^C@d4y zZX!&Tvs;Y8v+O5>&-0`Ei1vjye0fU$KSu;^&EuO%0V;tRpGVKSQO@gS^tF;Dv2|PBtC~qk)F#_8&1`qeWxzv7>z75Ar zw8-Q;NuK3Z<)rH(*HXMK`0Z>_Df#ZRZ zUzY?9&eKPFPMg`?r}eI5W`b9QxWfYcl83Zo`@Onwf-^75 zf}guDY4{r7sQ3!sqLWkvcm)Ij@vJ5yfjyi>_!e@tV*=tA4g$QHqp%IsqMKvQ6~I0` z%~=he@s$J1X=nl0{+)s{9IULGK+U0!NnG4Dg^)X6cz6TnyQfe$Jb^_!gW(|eFLzaj zgIvGdRmIuWAr7w&-N8q|P9>~#+{Wqve#g3<)nR4By-Cd-d`bZ|tkTA9Y^W%R)XFJ^ z-03ZJc4^_@@Fcd;-e(lA3)dCx)raef_8RWsItBEw?g|Wrb$4?}vQSZAVFZggt|Jfi z{O6#fiP5e^BigZ$gf8N(ZwYB#N@kYfAa{qb90@#zvuMURB|pS!Qoja2q6NR96;r%U z{)smHLkp7Go3vMpKhT5yoawlW1L!5&P571e4)I&8#82qsS_L(Jz+v<=H#_k?j&QA# z{;%UGM^%)*glBP#$u)!v7{GB<6UiBz;HZXp;uz$PN>s=39DlXM){SB6YDBmdBh;-U z&UJX6yY)o73@>ohK+KJJk-IuxBE}B-cYU3tFW2tNwCmb9NgJ-b0bQ>FI|8HGL%l_e zCBS!0$NEX!xP`?xIRQo3pgfUFwn{CL2yaIwok1)18ppBJmPvi!KS@N=^({PyW#T598gBfD ziiX8Ze|sd;yVaZJyQP*~nmyiErZ+D({17wW{amESyHiL=IgI#nhd9;?7-@ zt!^a!BsS1fv--2e9rR5E7+5GBQ(kH)(6!%dh;UtO22z8wE%t;Ph<@8$??i)FY1KfY z=8dOTodygp>Fp4ndS^@&hwvNM4p88=KVghv>JU;L8D*`lei+v`vUrU=wCb)f72rAWGN?qtI)yE_l|x0bbSL1vHQ!uD?Q6KxwmG} zT(YrPcJIxIvEn|Oi*u`_ZBncSXyv82Zcx9GDJ1jhnL?_NO}Fmvq_!vXyV*iEp9}Df zfK3