diff --git a/build.gradle.kts b/build.gradle.kts index 1c4bdc3..f13b24e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,7 +89,7 @@ val filtered = tasks.register("filteredTestClasspath") { dependencies { implementation(mainForNewTargets.output) - testImplementation(files(filtered.flatMap { it.outputDir })) + 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) diff --git a/src/main/java/io/papermc/asm/rules/RewriteRule.java b/src/main/java/io/papermc/asm/rules/RewriteRule.java index c1fe310..0510c4a 100644 --- a/src/main/java/io/papermc/asm/rules/RewriteRule.java +++ b/src/main/java/io/papermc/asm/rules/RewriteRule.java @@ -49,6 +49,10 @@ static RewriteRule chain(final RewriteRule... rules) { return chain(Arrays.asList(rules)); } + static RewriteRule chain(final RewriteRule rule1, final RewriteRule rule2) { + return chain(List.of(rule1, rule2)); + } + static RewriteRule chain(final Collection rules) { final List filteredRules = rules.stream().filter(r -> r != EMPTY).toList(); if (filteredRules.isEmpty()) { diff --git a/src/main/java/io/papermc/asm/rules/builder/RuleFactory.java b/src/main/java/io/papermc/asm/rules/builder/RuleFactory.java index ea909a2..f86c0a4 100644 --- a/src/main/java/io/papermc/asm/rules/builder/RuleFactory.java +++ b/src/main/java/io/papermc/asm/rules/builder/RuleFactory.java @@ -30,14 +30,11 @@ static RuleFactory.Factory combine(final RuleFactory.Factory... factories) { void plainStaticRewrite(ClassDesc newOwner, MethodMatcher methodMatcher, String staticMethodName); - default void changeParamToSuper(final Class oldParamType, final Class newParamType, final MethodMatcher methodMatcher) { - if (!newParamType.isAssignableFrom(oldParamType)) { - throw new IllegalArgumentException(newParamType + " is not a superclass of " + oldParamType); - } - this.changeParamToSuper(desc(oldParamType), desc(newParamType), methodMatcher); + default void changeParamToSuper(final Class newParamType, final TargetedMethodMatcher methodMatcher) { + this.changeParamToSuper( desc(newParamType), methodMatcher); } - void changeParamToSuper(ClassDesc legacyParamType, ClassDesc newParamType, MethodMatcher methodMatcher); + void changeParamToSuper(ClassDesc newParamType, TargetedMethodMatcher methodMatcher); default void changeParamFuzzy(final Class newParamType, final Method staticHandler, final TargetedMethodMatcher targetedMethodMatcher) { this.changeParamFuzzy(desc(newParamType), staticHandler, targetedMethodMatcher); @@ -51,14 +48,11 @@ default void changeParamDirect(final Class newParamType, final Method staticH void changeParamDirect(ClassDesc newParamType, Method staticHandler, TargetedMethodMatcher targetedMethodMatcher); - default void changeReturnTypeToSub(final Class oldReturnType, final Class newReturnType, final MethodMatcher methodMatcher) { - if (!oldReturnType.isAssignableFrom(newReturnType)) { - throw new IllegalArgumentException(newReturnType + " is not a subclass of " + oldReturnType); - } - this.changeReturnTypeToSub(desc(oldReturnType), desc(newReturnType), methodMatcher); + default void changeReturnTypeToSub(final Class newReturnType, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeToSub(desc(newReturnType), methodMatcher); } - void changeReturnTypeToSub(ClassDesc oldReturnType, ClassDesc newReturnType, MethodMatcher methodMatcher); + void changeReturnTypeToSub(ClassDesc newReturnType, TargetedMethodMatcher methodMatcher); default void changeReturnTypeDirect(final Class newReturnType, final Method staticHandler, final TargetedMethodMatcher targetedMethodMatcher) { this.changeReturnTypeDirect(desc(newReturnType), staticHandler, targetedMethodMatcher); diff --git a/src/main/java/io/papermc/asm/rules/builder/RuleFactoryImpl.java b/src/main/java/io/papermc/asm/rules/builder/RuleFactoryImpl.java index 4e7ed90..3dabe7e 100644 --- a/src/main/java/io/papermc/asm/rules/builder/RuleFactoryImpl.java +++ b/src/main/java/io/papermc/asm/rules/builder/RuleFactoryImpl.java @@ -50,8 +50,8 @@ public void plainStaticRewrite(final ClassDesc newOwner, final MethodMatcher met } @Override - public void changeParamToSuper(final ClassDesc legacyParamType, final ClassDesc newParamType, final MethodMatcher methodMatcher) { - this.addRule(new SuperTypeParamRewrite(this.owners, methodMatcher, legacyParamType, newParamType)); + public void changeParamToSuper(final ClassDesc newParamType, final TargetedMethodMatcher methodMatcher) { + this.addRule(new SuperTypeParamRewrite(this.owners, methodMatcher, newParamType)); } @Override @@ -65,8 +65,8 @@ public void changeParamDirect(final ClassDesc newParamType, final Method staticH } @Override - public void changeReturnTypeToSub(final ClassDesc oldReturnType, final ClassDesc newReturnType, final MethodMatcher methodMatcher) { - this.addRule(new SubTypeReturnRewrite(this.owners, methodMatcher, oldReturnType, newReturnType)); + public void changeReturnTypeToSub(final ClassDesc newReturnType, final TargetedMethodMatcher methodMatcher) { + this.addRule(new SubTypeReturnRewrite(this.owners, methodMatcher, newReturnType)); } @Override diff --git a/src/main/java/io/papermc/asm/rules/field/FieldToMethodRewrite.java b/src/main/java/io/papermc/asm/rules/field/FieldToMethodRewrite.java index 008614c..f973d27 100644 --- a/src/main/java/io/papermc/asm/rules/field/FieldToMethodRewrite.java +++ b/src/main/java/io/papermc/asm/rules/field/FieldToMethodRewrite.java @@ -1,7 +1,11 @@ package io.papermc.asm.rules.field; import io.papermc.asm.ClassProcessingContext; +import io.papermc.asm.rules.RewriteRule; import io.papermc.asm.rules.builder.matcher.field.FieldMatcher; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.constant.ConstantDescs; import java.lang.constant.MethodTypeDesc; @@ -91,4 +95,18 @@ MethodTypeDesc desc(final ClassDesc fieldTypeDesc) { delegate.visitMethodInsn(type.opcode(opcode), owner, methodName, type.desc(fieldTypeDesc).descriptorString(), this.isInterfaceMethod); }; } + + public record Versioned(Set owners, @Nullable String getterName, @Nullable String setterName, boolean isInterfaceMethod, VersionedMatcher versions) implements VersionedRuleFactory { + + public Versioned { + if (getterName == null && setterName == null) { + throw new IllegalArgumentException("At least one of getterName or setterName must be non-null"); + } + } + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, matcher -> new FieldToMethodRewrite(this.owners(), matcher, this.getterName(), this.setterName(), this.isInterfaceMethod())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/method/DirectStaticRewrite.java b/src/main/java/io/papermc/asm/rules/method/DirectStaticRewrite.java index c6095ce..3d19c15 100644 --- a/src/main/java/io/papermc/asm/rules/method/DirectStaticRewrite.java +++ b/src/main/java/io/papermc/asm/rules/method/DirectStaticRewrite.java @@ -1,11 +1,15 @@ package io.papermc.asm.rules.method; import io.papermc.asm.ClassProcessingContext; +import io.papermc.asm.rules.RewriteRule; import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; import io.papermc.asm.rules.generate.GeneratedMethodHolder; import io.papermc.asm.rules.method.rewrite.ConstructorRewrite; import io.papermc.asm.rules.method.rewrite.MethodRewrite; import io.papermc.asm.rules.method.rewrite.SimpleRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.util.Set; @@ -49,4 +53,12 @@ public MethodRewrite createConstructo public ClassDesc staticRedirectOwner(final ClassProcessingContext context) { return this.staticRedirectOwner; } + + public record Versioned(Set owners, ClassDesc staticRedirectOwner, @Nullable String staticMethodName, VersionedMatcher versions) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, match -> new DirectStaticRewrite(this.owners(), this.staticMethodName(), match, this.staticRedirectOwner())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/method/MoveInstanceMethod.java b/src/main/java/io/papermc/asm/rules/method/MoveInstanceMethod.java index f58c55a..0fbae46 100644 --- a/src/main/java/io/papermc/asm/rules/method/MoveInstanceMethod.java +++ b/src/main/java/io/papermc/asm/rules/method/MoveInstanceMethod.java @@ -1,9 +1,13 @@ package io.papermc.asm.rules.method; import io.papermc.asm.ClassProcessingContext; +import io.papermc.asm.rules.RewriteRule; import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; import io.papermc.asm.rules.method.generated.GeneratedStaticRewrite; import io.papermc.asm.rules.method.rewrite.MethodRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.util.Set; @@ -56,4 +60,12 @@ public void generateMethod(final GeneratorAdapterFactory factory, final MethodCa public void generateConstructor(final GeneratorAdapterFactory factory, final MethodCallData modified, final ConstructorCallData original) { throw new UnsupportedOperationException("Doesn't work with constructors"); } + + public record Versioned(Set owners, ClassDesc newOwner, String newMethodName, VersionedMatcher versions) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, match -> new MoveInstanceMethod(this.owners(), match, this.newOwner(), this.newMethodName())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/method/params/DirectParameterRewrite.java b/src/main/java/io/papermc/asm/rules/method/params/DirectParameterRewrite.java index 68591fd..21d9562 100644 --- a/src/main/java/io/papermc/asm/rules/method/params/DirectParameterRewrite.java +++ b/src/main/java/io/papermc/asm/rules/method/params/DirectParameterRewrite.java @@ -1,8 +1,13 @@ package io.papermc.asm.rules.method.params; +import io.papermc.asm.rules.RewriteRule; import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; import io.papermc.asm.rules.method.OwnableMethodRewriteRule; import io.papermc.asm.rules.method.generated.TargetedTypeGeneratedStaticRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.TargetedMethodMatcherWithHandler; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.reflect.Method; import java.util.Set; @@ -17,4 +22,12 @@ * @param staticHandler the method which will be used to convert the legacy type to the new type */ public record DirectParameterRewrite(Set owners, ClassDesc existingType, TargetedMethodMatcher methodMatcher, Method staticHandler) implements TargetedTypeGeneratedStaticRewrite.Parameter, OwnableMethodRewriteRule.Filtered { + + public record Versioned(Set owners, ClassDesc existingType, VersionedMatcher versions) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, pair -> new DirectParameterRewrite(this.owners, this.existingType, pair.matcher(), pair.staticHandler())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/method/params/FuzzyParameterRewrite.java b/src/main/java/io/papermc/asm/rules/method/params/FuzzyParameterRewrite.java index 60d2504..7e7abd6 100644 --- a/src/main/java/io/papermc/asm/rules/method/params/FuzzyParameterRewrite.java +++ b/src/main/java/io/papermc/asm/rules/method/params/FuzzyParameterRewrite.java @@ -1,10 +1,15 @@ package io.papermc.asm.rules.method.params; import io.papermc.asm.ClassProcessingContext; +import io.papermc.asm.rules.RewriteRule; import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; import io.papermc.asm.rules.method.OwnableMethodRewriteRule; import io.papermc.asm.rules.method.generated.TargetedTypeGeneratedStaticRewrite; import io.papermc.asm.rules.method.rewrite.MethodRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.TargetedMethodMatcherWithHandler; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.constant.ConstantDescs; import java.lang.constant.MethodTypeDesc; @@ -54,4 +59,12 @@ public MethodRewrite createRewrite(final ClassProcessingContext arguments[MethodRewrite.DYNAMIC_TYPE_IDX] = Type.getMethodType(newDynamicMethodType.descriptorString()); }); } + + public record Versioned(Set owners, ClassDesc existingType, VersionedMatcher versions) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, pair -> new FuzzyParameterRewrite(this.owners, this.existingType, pair.matcher(), pair.staticHandler())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/method/params/SuperTypeParamRewrite.java b/src/main/java/io/papermc/asm/rules/method/params/SuperTypeParamRewrite.java index 414136d..8bed8b4 100644 --- a/src/main/java/io/papermc/asm/rules/method/params/SuperTypeParamRewrite.java +++ b/src/main/java/io/papermc/asm/rules/method/params/SuperTypeParamRewrite.java @@ -1,10 +1,14 @@ package io.papermc.asm.rules.method.params; import io.papermc.asm.ClassProcessingContext; -import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; +import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; import io.papermc.asm.rules.method.OwnableMethodRewriteRule; import io.papermc.asm.rules.method.rewrite.MethodRewrite; import io.papermc.asm.rules.method.rewrite.SimpleRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.util.Set; @@ -17,11 +21,14 @@ * offending parameter in the descriptor and move on. * * @param owners owners of the methods to change - * @param methodMatcher method matcher to find methods with - * @param oldParamType the parameter type that will be found in bytecode that needs to be transformed + * @param methodMatcher method matcher to find methods with (target is the type to be found in bytecode that needs to be transformed) * @param newParamType the parameter type that is valid for existing method */ -public record SuperTypeParamRewrite(Set owners, MethodMatcher methodMatcher, ClassDesc oldParamType, ClassDesc newParamType) implements OwnableMethodRewriteRule.Filtered { +public record SuperTypeParamRewrite(Set owners, TargetedMethodMatcher methodMatcher, ClassDesc newParamType) implements OwnableMethodRewriteRule.Filtered { + + public ClassDesc oldParamType() { + return this.methodMatcher.targetType(); + } @Override public MethodRewrite rewrite(final ClassProcessingContext context, final boolean isInvokeDynamic, final int opcode, final ClassDesc owner, final String name, final MethodTypeDesc descriptor, final boolean isInterface) { @@ -29,6 +36,14 @@ public MethodRewrite rewrite(final ClassProcessingContext context, final bool } private MethodTypeDesc modifyMethodDescriptor(final MethodTypeDesc methodDescriptor) { - return replaceParameters(methodDescriptor, isEqual(this.oldParamType()), this.newParamType()); + return replaceParameters(methodDescriptor, isEqual(this.methodMatcher().targetType()), this.newParamType()); + } + + public record Versioned(Set owners, ClassDesc newParamType, VersionedMatcher versions) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, matcher -> new SuperTypeParamRewrite(this.owners(), matcher, this.newParamType())); + } } } diff --git a/src/main/java/io/papermc/asm/rules/method/returns/DirectReturnRewrite.java b/src/main/java/io/papermc/asm/rules/method/returns/DirectReturnRewrite.java index 7790820..d8be19f 100644 --- a/src/main/java/io/papermc/asm/rules/method/returns/DirectReturnRewrite.java +++ b/src/main/java/io/papermc/asm/rules/method/returns/DirectReturnRewrite.java @@ -1,8 +1,13 @@ package io.papermc.asm.rules.method.returns; +import io.papermc.asm.rules.RewriteRule; import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; import io.papermc.asm.rules.method.OwnableMethodRewriteRule; import io.papermc.asm.rules.method.generated.TargetedTypeGeneratedStaticRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.TargetedMethodMatcherWithHandler; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.reflect.Method; import java.util.Set; @@ -30,4 +35,12 @@ public record DirectReturnRewrite(Set owners, ClassDesc existingType, throw new IllegalArgumentException("staticHandler param type isn't " + existingType); } } + + public record Versioned(Set owners, ClassDesc existingType, VersionedMatcher versions, boolean includeOwnerContext) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, pair -> new DirectReturnRewrite(this.owners(), this.existingType(), pair.matcher(), pair.staticHandler(), this.includeOwnerContext())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/method/returns/SubTypeReturnRewrite.java b/src/main/java/io/papermc/asm/rules/method/returns/SubTypeReturnRewrite.java index c8acfd7..1c2db57 100644 --- a/src/main/java/io/papermc/asm/rules/method/returns/SubTypeReturnRewrite.java +++ b/src/main/java/io/papermc/asm/rules/method/returns/SubTypeReturnRewrite.java @@ -1,10 +1,14 @@ package io.papermc.asm.rules.method.returns; import io.papermc.asm.ClassProcessingContext; -import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; +import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; import io.papermc.asm.rules.method.OwnableMethodRewriteRule; import io.papermc.asm.rules.method.rewrite.MethodRewrite; import io.papermc.asm.rules.method.rewrite.SimpleRewrite; +import io.papermc.asm.versioned.ApiVersion; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.util.Set; @@ -15,15 +19,18 @@ * We just change the return type in the descriptor and move on. * * @param owners owners of the methods to change - * @param methodMatcher method matcher to find methods with - * @param oldReturnType the return type that will be found in bytecode that needs to be transformed + * @param methodMatcher method matcher to find methods with (target is the type to be found in bytecode that needs to be transformed) * @param newReturnType the return type that is valid for existing method */ -public record SubTypeReturnRewrite(Set owners, MethodMatcher methodMatcher, ClassDesc oldReturnType, ClassDesc newReturnType) implements OwnableMethodRewriteRule.Filtered { +public record SubTypeReturnRewrite(Set owners, TargetedMethodMatcher methodMatcher, ClassDesc newReturnType) implements OwnableMethodRewriteRule.Filtered { + + public ClassDesc oldReturnType() { + return this.methodMatcher.targetType(); + } @Override public @Nullable MethodRewrite rewrite(final ClassProcessingContext context, final boolean isInvokeDynamic, final int opcode, final ClassDesc owner, final String name, final MethodTypeDesc descriptor, final boolean isInterface) { - if (!descriptor.returnType().equals(this.newReturnType())) { + if (descriptor.returnType().equals(this.methodMatcher().targetType())) { return new SimpleRewrite(opcode, owner, name, this.modifyMethodDescriptor(descriptor), isInterface, isInvokeDynamic); } return null; @@ -32,4 +39,12 @@ public record SubTypeReturnRewrite(Set owners, MethodMatcher methodMa private MethodTypeDesc modifyMethodDescriptor(final MethodTypeDesc methodDescriptor) { return methodDescriptor.changeReturnType(this.newReturnType()); } + + public record Versioned(Set owners, ClassDesc newReturnType, VersionedMatcher versions) implements VersionedRuleFactory { + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + return this.versions.ruleForVersion(apiVersion, matcher -> new SubTypeReturnRewrite(this.owners(), matcher, this.newReturnType())); + } + } } diff --git a/src/main/java/io/papermc/asm/rules/rename/EnumRenameBuilder.java b/src/main/java/io/papermc/asm/rules/rename/EnumRenameBuilder.java new file mode 100644 index 0000000..2de4403 --- /dev/null +++ b/src/main/java/io/papermc/asm/rules/rename/EnumRenameBuilder.java @@ -0,0 +1,40 @@ +package io.papermc.asm.rules.rename; + +import java.lang.constant.ClassDesc; +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static io.papermc.asm.util.DescriptorUtils.desc; + +public final class EnumRenameBuilder { + + private final ClassDesc enumTypeDesc; + private @Nullable ClassDesc alternateValueOfOwner; + private final Map enumFieldRenames = new HashMap<>(); + + EnumRenameBuilder(final ClassDesc enumTypeDesc) { + this.enumTypeDesc = enumTypeDesc; + } + + public EnumRenameBuilder alternateValueOfOwner(final Class type) { + return this.alternateValueOfOwner(desc(type)); + } + + public EnumRenameBuilder alternateValueOfOwner(final ClassDesc type) { + if (this.enumTypeDesc.equals(type)) { + throw new IllegalArgumentException("Cannot replace an enum with itself"); + } + this.alternateValueOfOwner = type; + return this; + } + + public EnumRenameBuilder rename(final String legacyName, final String newName) { + this.enumFieldRenames.put(legacyName, newName); + return this; + } + + EnumRenamer build() { + return new EnumRenamer(this.enumTypeDesc, this.alternateValueOfOwner, Map.copyOf(this.enumFieldRenames)); + } +} diff --git a/src/main/java/io/papermc/asm/rules/rename/EnumRenamer.java b/src/main/java/io/papermc/asm/rules/rename/EnumRenamer.java new file mode 100644 index 0000000..b77d915 --- /dev/null +++ b/src/main/java/io/papermc/asm/rules/rename/EnumRenamer.java @@ -0,0 +1,26 @@ +package io.papermc.asm.rules.rename; + +import java.lang.constant.ClassDesc; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +public record EnumRenamer(ClassDesc typeDesc, @Nullable ClassDesc alternateValueOfOwner, Map fieldRenames) { + + public EnumRenamer { + fieldRenames = Map.copyOf(fieldRenames); + } + + EnumRenamer overwrite(final EnumRenamer other) { + if (!this.typeDesc.equals(other.typeDesc)) { + throw new IllegalArgumentException("Cannot merge EnumRenamers with different typeDesc"); + } else if (!Objects.equals(this.alternateValueOfOwner, other.alternateValueOfOwner)) { + throw new IllegalArgumentException("Cannot merge EnumRenamers with different alternateValueOfOwner"); + } + final Map newFieldRenames = new HashMap<>(); + newFieldRenames.putAll(this.fieldRenames); + newFieldRenames.putAll(other.fieldRenames); + return new EnumRenamer(this.typeDesc, this.alternateValueOfOwner, newFieldRenames); + } +} diff --git a/src/main/java/io/papermc/asm/rules/rename/EnumValueOfRewriteRule.java b/src/main/java/io/papermc/asm/rules/rename/EnumValueOfRewriteRule.java new file mode 100644 index 0000000..086cde4 --- /dev/null +++ b/src/main/java/io/papermc/asm/rules/rename/EnumValueOfRewriteRule.java @@ -0,0 +1,144 @@ +package io.papermc.asm.rules.rename; + +import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; +import io.papermc.asm.rules.generate.GeneratedMethodHolder; +import io.papermc.asm.rules.method.OwnableMethodRewriteRule; +import io.papermc.asm.rules.method.generated.GeneratedStaticRewrite; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.lang.constant.MethodTypeDesc; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +final class EnumValueOfRewriteRule implements GeneratedStaticRewrite, OwnableMethodRewriteRule.Filtered { + + private final Set owners = new HashSet<>(); + private final MethodMatcher matcher; + private final Map fieldRenames; + + EnumValueOfRewriteRule(final EnumRenamer renamer) { + this.owners.add(renamer.typeDesc()); + if (renamer.alternateValueOfOwner() != null) { + this.owners.add(renamer.alternateValueOfOwner()); + } + this.matcher = MethodMatcher.builder() + .match("valueOf", b -> b.statik().desc(MethodTypeDesc.of(renamer.typeDesc(), ConstantDescs.CD_String))) + .build(); + this.fieldRenames = new TreeMap<>(renamer.fieldRenames()); + } + + @Override + public void generateMethod(final GeneratorAdapterFactory factory, final MethodCallData modified, final MethodCallData original) { + final GeneratorAdapter methodGenerator = this.createAdapter(factory, modified); + GeneratedMethodHolder.loadParameters(methodGenerator, modified.descriptor()); + final int tableSwitchIndexLocal = methodGenerator.newLocal(Type.INT_TYPE); + methodGenerator.push(-1); + methodGenerator.storeLocal(tableSwitchIndexLocal); + methodGenerator.loadArg(0); + methodGenerator.invokeVirtual(Type.getType(String.class), new Method("hashCode", "()I")); + final Map tableSwitchIndexMap = new LinkedHashMap<>(); + final String[] tableSwitchIndexToRenamedField = new String[this.fieldRenames.size()]; + final Map> hashToField = new LinkedHashMap<>(); + int curIdx = 0; + for (final Map.Entry entry : this.fieldRenames.entrySet()) { + tableSwitchIndexMap.put(entry.getKey(), curIdx); + tableSwitchIndexToRenamedField[curIdx] = entry.getValue(); + curIdx++; + hashToField.computeIfAbsent(entry.getKey().hashCode(), k -> new ArrayList<>()).add(entry.getKey()); + } + final int[] lookupSwitchKeys = hashToField.keySet().stream().mapToInt(Integer::intValue).toArray(); + Arrays.sort(lookupSwitchKeys); + final Label lookupSwitchEndLabel = methodGenerator.newLabel(); // is also default label in this case + final Label[] labels = new Label[lookupSwitchKeys.length]; + for (int i = 0; i < labels.length; i++) { + labels[i] = methodGenerator.newLabel(); + } + methodGenerator.visitLookupSwitchInsn(lookupSwitchEndLabel, lookupSwitchKeys, labels); + for (int i = 0; i < labels.length; i++) { + methodGenerator.mark(labels[i]); + // LocalVariableSorter will insert the trailing int local for this and all following visitFrame calls; adding it manually would cause duplicate locals in the frame + methodGenerator.visitFrame(Opcodes.F_NEW, 1, new Object[]{"java/lang/String"}, 1, new Object[]{"java/lang/String"}); + // generate case + final List matchingStrings = hashToField.get(lookupSwitchKeys[i]); + if (matchingStrings.size() == 1) { + methodGenerator.loadArg(0); // load pass string arg + methodGenerator.push(matchingStrings.get(0)); // load maybe matching string + methodGenerator.invokeVirtual(Type.getType(String.class), new Method("equals", "(Ljava/lang/Object;)Z")); + methodGenerator.visitJumpInsn(Opcodes.IFEQ, lookupSwitchEndLabel); + methodGenerator.push(tableSwitchIndexMap.get(matchingStrings.get(0))); + methodGenerator.storeLocal(tableSwitchIndexLocal); + methodGenerator.goTo(lookupSwitchEndLabel); + } else { + final Label[] nestedLabels = new Label[matchingStrings.size()]; + for (int j = 0; j < matchingStrings.size() - 1; j++) { + nestedLabels[j] = methodGenerator.newLabel(); + } + nestedLabels[matchingStrings.size() - 1] = lookupSwitchEndLabel; + for (int j = 0; j < matchingStrings.size(); j++) { + final String maybeMatchingString = matchingStrings.get(j); + methodGenerator.loadArg(0); // load pass string arg + methodGenerator.push(maybeMatchingString); + methodGenerator.invokeVirtual(Type.getType(String.class), new Method("equals", "(Ljava/lang/Object;)Z")); + methodGenerator.visitJumpInsn(Opcodes.IFEQ, nestedLabels[j]); + methodGenerator.push(tableSwitchIndexMap.get(maybeMatchingString)); + methodGenerator.storeLocal(tableSwitchIndexLocal); + methodGenerator.goTo(lookupSwitchEndLabel); + if (nestedLabels[j] != lookupSwitchEndLabel) { + methodGenerator.mark(nestedLabels[j]); // mark start of next label (except last one) + methodGenerator.visitFrame(Opcodes.F_NEW, 1, new Object[]{"java/lang/String"}, 1, new Object[]{"java/lang/String"}); + } + } + } + } + methodGenerator.mark(lookupSwitchEndLabel); + methodGenerator.visitFrame(Opcodes.F_NEW, 1, new Object[]{"java/lang/String"}, 1, new Object[]{"java/lang/String"}); + methodGenerator.loadLocal(tableSwitchIndexLocal); + final Label[] tableSwitchLabels = new Label[tableSwitchIndexToRenamedField.length]; + for (int i = 0; i < tableSwitchLabels.length; i++) { + tableSwitchLabels[i] = methodGenerator.newLabel(); + } + final Label tableSwitchDefaultLabel = methodGenerator.newLabel(); + final Label tableSwitchEndLabel = methodGenerator.newLabel(); + methodGenerator.visitTableSwitchInsn(0, tableSwitchIndexToRenamedField.length - 1, tableSwitchDefaultLabel, tableSwitchLabels); + for (int i = 0; i < tableSwitchIndexToRenamedField.length; i++) { + methodGenerator.mark(tableSwitchLabels[i]); + methodGenerator.visitFrame(Opcodes.F_NEW, 1, new Object[]{"java/lang/String"}, 1, new Object[]{"java/lang/String"}); + methodGenerator.push(tableSwitchIndexToRenamedField[i]); + methodGenerator.goTo(tableSwitchEndLabel); + } + methodGenerator.mark(tableSwitchDefaultLabel); + methodGenerator.visitFrame(Opcodes.F_NEW, 1, new Object[]{"java/lang/String"}, 1, new Object[]{"java/lang/String"}); + methodGenerator.loadArg(0); // default to the passed in value + methodGenerator.mark(tableSwitchEndLabel); + methodGenerator.visitFrame(Opcodes.F_NEW, 1, new Object[]{"java/lang/String"}, 2, new Object[]{"java/lang/String", "java/lang/String"}); + methodGenerator.invokeStatic(Type.getType(original.owner().descriptorString()), new Method(original.name(), original.descriptor().descriptorString())); + methodGenerator.returnValue(); + methodGenerator.endMethod(); + } + + @Override + public void generateConstructor(final GeneratorAdapterFactory factory, final MethodCallData modified, final ConstructorCallData original) { + throw new UnsupportedOperationException("EnumValueOfRewriteRule does not support constructor generation"); + } + + @Override + public MethodMatcher methodMatcher() { + return this.matcher; + } + + @Override + public Set owners() { + return this.owners; + } +} diff --git a/src/main/java/io/papermc/asm/rules/rename/RenameRule.java b/src/main/java/io/papermc/asm/rules/rename/RenameRule.java index 1f5f89c..c86eb48 100644 --- a/src/main/java/io/papermc/asm/rules/rename/RenameRule.java +++ b/src/main/java/io/papermc/asm/rules/rename/RenameRule.java @@ -1,126 +1,64 @@ package io.papermc.asm.rules.rename; -import io.papermc.asm.ClassProcessingContext; import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.rules.rename.asm.FixedClassRemapper; +import io.papermc.asm.versioned.Mergeable; import java.lang.constant.ClassDesc; -import java.lang.constant.MethodTypeDesc; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; -import org.objectweb.asm.ClassVisitor; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.commons.SimpleRemapper; -import static io.papermc.asm.util.DescriptorUtils.toOwner; +public final class RenameRule implements RewriteRule.Delegate, Mergeable { -/** - * Applies the provided {@link Remapper} to the bytecode. - * This is better than {@code asm-commons}'s {@link org.objectweb.asm.commons.ClassRemapper} because this - * remaps enum names in annotations. - * - * @param remapper the remapper to apply - */ -public record RenameRule(Remapper remapper) implements RewriteRule { - - public static Builder builder() { - return new Builder(); - } - - @Override - public ClassVisitor createVisitor(final int api, final ClassVisitor parent, final ClassProcessingContext context) { - return new FixedClassRemapper(api, parent, this.remapper); + public static RenameRuleBuilder builder() { + return new RenameRuleBuilderImpl(); } - public static final class Builder implements io.papermc.asm.util.Builder { - - private Builder() { - } - - private final Map mappings = new HashMap<>(); - - public Builder methodByDesc(final Iterable owners, final String legacyMethodName, final MethodTypeDesc desc, final String newMethodName) { - for (final ClassDesc owner : owners) { - this.methodByDesc(owner, legacyMethodName, desc, newMethodName); - } - return this; - } - - public Builder methodByDesc(final ClassDesc owner, final String legacyMethodName, final MethodTypeDesc desc, final String newMethodName) { - return this.methodByInternal(toOwner(owner), legacyMethodName, desc.descriptorString(), newMethodName); - } - - public Builder methodByInternal(final Iterable owners, final String legacyMethodName, final MethodTypeDesc desc, final String newMethodName) { - for (final String owner : owners) { - this.methodByInternal(owner, legacyMethodName, desc.descriptorString(), newMethodName); - } - return this; - } - - public Builder methodByInternal(final String owner, final String legacyMethodName, final String desc, final String newMethodName) { - this.mappings.put("%s.%s%s".formatted(owner, legacyMethodName, desc), newMethodName); - return this; - } + private final Map renames; + private final Map enumFieldRenames; + private @MonotonicNonNull RewriteRule rule; - public Builder fieldsByDesc(final Iterable owners, final String legacyFieldName, final String newFieldName) { - for (final ClassDesc owner : owners) { - this.fieldByDesc(owner, legacyFieldName, newFieldName); - } - return this; - } - - public Builder fieldByDesc(final ClassDesc owner, final String legacyFieldName, final String newFieldName) { - return this.fieldByInternal(toOwner(owner), legacyFieldName, newFieldName); - } - - public Builder fieldByInternal(final Iterable owners, final String legacyFieldName, final String newFieldName) { - for (final String owner : owners) { - this.fieldByInternal(owner, legacyFieldName, newFieldName); - } - return this; - } + public RenameRule(final Map renames, final Map enumFieldRenames) { + this.renames = Map.copyOf(renames); + this.enumFieldRenames = Map.copyOf(enumFieldRenames); + } - public Builder fieldByInternal(final String owner, final String legacyFieldName, final String newFieldName) { - this.mappings.put("%s.%s".formatted(owner, legacyFieldName), newFieldName); - return this; - } + public Map renames() { + return this.renames; + } - /** - * Note that you also have to remap the method for the annotation attribute. - */ - public Builder annotationAttribute(final ClassDesc owner, final String legacyName, final String newName) { - return this.annotationAttribute(owner.descriptorString(), legacyName, newName); - } + public Map enumFieldRenames() { + return this.enumFieldRenames; + } - /** - * Note that you also have to remap the method for the annotation attribute. - */ - public Builder annotationAttribute(final String ownerDescriptor, final String legacyName, final String newName) { - if (!ownerDescriptor.startsWith("L") || !ownerDescriptor.endsWith(";")) { - throw new IllegalArgumentException("Invalid owner descriptor: %s".formatted(ownerDescriptor)); - } - // for some reason the remapper wants the Lpkg/name; format, but just for annotation attributes - this.mappings.put("%s.%s".formatted(ownerDescriptor, legacyName), newName); - return this; - } + @Override + public RewriteRule delegate() { + if (this.rule == null) { + final Remapper remapper = new SimpleRemapper(Map.copyOf(this.renames)); - /** - * Use {@code /} instead of {@code .}. - */ - public Builder type(final String legacyType, final ClassDesc newType) { - this.mappings.put(legacyType, toOwner(newType)); - return this; - } + final List rules = new ArrayList<>(this.enumFieldRenames.size() + 1); + this.enumFieldRenames.forEach((classDesc, enumRenamer) -> { + rules.add(new EnumValueOfRewriteRule(enumRenamer)); + }); + rules.add((api, parent, context) -> new FixedClassRemapper(api, parent, remapper)); - /** - * Use {@code /} instead of {@code .}. - */ - public Builder type(final String legacyType, final String newType) { - this.mappings.put(legacyType, newType); - return this; + this.rule = RewriteRule.chain(rules); } + return this.rule; + } - @Override - public RenameRule build() { - return new RenameRule(new SimpleRemapper(Map.copyOf(this.mappings))); - } + @Override + public RenameRule merge(final RenameRule other) { + final Map regularRenames = new HashMap<>(this.renames); + final Map enumFieldRenames = new HashMap<>(this.enumFieldRenames); + regularRenames.putAll(other.renames); + other.enumFieldRenames.forEach((cd, renamer) -> { + enumFieldRenames.merge(cd, renamer, EnumRenamer::overwrite); + }); + return new RenameRule(regularRenames, enumFieldRenames); } } diff --git a/src/main/java/io/papermc/asm/rules/rename/RenameRuleBuilder.java b/src/main/java/io/papermc/asm/rules/rename/RenameRuleBuilder.java new file mode 100644 index 0000000..3db6c97 --- /dev/null +++ b/src/main/java/io/papermc/asm/rules/rename/RenameRuleBuilder.java @@ -0,0 +1,86 @@ +package io.papermc.asm.rules.rename; + +import io.papermc.asm.util.Builder; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.Set; +import java.util.function.Consumer; + +import static io.papermc.asm.util.DescriptorUtils.desc; + +public interface RenameRuleBuilder extends Builder { + + // + default RenameRuleBuilder methodByClass(final Set> owners, final String legacyMethodName, final MethodTypeDesc methodDesc, final String newMethodName) { + for (final Class owner : owners) { + this.methodByClass(owner, legacyMethodName, methodDesc, newMethodName); + } + return this; + } + + default RenameRuleBuilder methodByClass(final Class owner, final String legacyMethodName, final MethodTypeDesc methodDesc, final String newMethodName) { + return this.method(desc(owner), legacyMethodName, methodDesc, newMethodName); + } + + default RenameRuleBuilder method(final Set owners, final String legacyMethodName, final MethodTypeDesc methodDesc, final String newMethodName) { + for (final ClassDesc owner : owners) { + this.method(owner, legacyMethodName, methodDesc, newMethodName); + } + return this; + } + + RenameRuleBuilder method(ClassDesc owner, String legacyMethodName, MethodTypeDesc methodDesc, final String newMethodName); + // + + // + default RenameRuleBuilder fieldByClass(final Set> owners, final String legacyFieldName, final String newFieldName) { + for (final Class owner : owners) { + this.fieldByClass(owner, legacyFieldName, newFieldName); + } + return this; + } + + default RenameRuleBuilder fieldByClass(final Class owner, final String legacyFieldName, final String newFieldName) { + return this.field(desc(owner), legacyFieldName, newFieldName); + } + + default RenameRuleBuilder field(final Set owners, final String legacyFieldName, final String newFieldName) { + for (final ClassDesc owner : owners) { + this.field(owner, legacyFieldName, newFieldName); + } + return this; + } + + RenameRuleBuilder field(ClassDesc owner, String legacyFieldName, String newFieldName); + // + + /** + * Note that you also have to remap the method for the annotation attribute. + */ + default RenameRuleBuilder annotationAttribute(final Class owner, final String legacyName, final String newName) { + return this.annotationAttribute(desc(owner), legacyName, newName); + } + + /** + * Note that you also have to remap the method for the annotation attribute. + */ + RenameRuleBuilder annotationAttribute(ClassDesc owner, String legacyName, String newName); + + /** + * Use {@code /} instead of {@code .}. + */ + default RenameRuleBuilder type(final String legacyType, final Class newType) { + return this.type(legacyType, desc(newType)); + } + + /** + * Use {@code /} instead of {@code .}. + */ + RenameRuleBuilder type(String legacyType, ClassDesc newType); + + default RenameRuleBuilder editEnum(final Class enumType, final Consumer renameBuilder) { + return this.editEnum(desc(enumType), renameBuilder); + } + + RenameRuleBuilder editEnum(ClassDesc enumTypeDesc, Consumer renameBuilder); +} diff --git a/src/main/java/io/papermc/asm/rules/rename/RenameRuleBuilderImpl.java b/src/main/java/io/papermc/asm/rules/rename/RenameRuleBuilderImpl.java new file mode 100644 index 0000000..2cdafed --- /dev/null +++ b/src/main/java/io/papermc/asm/rules/rename/RenameRuleBuilderImpl.java @@ -0,0 +1,64 @@ +package io.papermc.asm.rules.rename; + +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import static io.papermc.asm.util.DescriptorUtils.toOwner; + +final class RenameRuleBuilderImpl implements RenameRuleBuilder { + + RenameRuleBuilderImpl() { + } + + final Map mappings = new HashMap<>(); + final Map enumValueOfFieldRenames = new HashMap<>(); + + @Override + public RenameRuleBuilder method(final ClassDesc owner, final String legacyMethodName, final MethodTypeDesc methodDesc, final String newMethodName) { + this.mappings.put("%s.%s%s".formatted(toOwner(owner), legacyMethodName, methodDesc.descriptorString()), newMethodName); + return this; + } + + @Override + public RenameRuleBuilder field(final ClassDesc owner, final String legacyFieldName, final String newFieldName) { + this.mappings.put("%s.%s".formatted(toOwner(owner), legacyFieldName), newFieldName); + return this; + } + + @Override + public RenameRuleBuilder annotationAttribute(final ClassDesc owner, final String legacyName, final String newName) { + final String ownerDescriptor = owner.descriptorString(); + if (!ownerDescriptor.startsWith("L") || !ownerDescriptor.endsWith(";")) { + throw new IllegalArgumentException("Invalid owner descriptor: %s".formatted(ownerDescriptor)); + } + // for some reason the remapper wants the Lpkg/name; format, but just for annotation attributes + this.mappings.put("%s.%s".formatted(ownerDescriptor, legacyName), newName); + return this; + } + + @Override + public RenameRuleBuilder type(final String legacyType, final ClassDesc newType) { + this.mappings.put(legacyType, toOwner(newType)); + return this; + } + + @Override + public RenameRuleBuilder editEnum(final ClassDesc enumTypeDesc, final Consumer renamer) { + final EnumRenameBuilder enumRenamerBuilder = new EnumRenameBuilder(enumTypeDesc); + renamer.accept(enumRenamerBuilder); + final EnumRenamer enumRenamer = enumRenamerBuilder.build(); + enumRenamer.fieldRenames().forEach((legacyName, newName) -> { + this.field(enumTypeDesc, legacyName, newName); + }); + this.enumValueOfFieldRenames.put(enumTypeDesc, enumRenamer); + return this; + } + + @Override + public RenameRule build() { + return new RenameRule(this.mappings, this.enumValueOfFieldRenames); + } +} diff --git a/src/main/java/io/papermc/asm/rules/rename/FixedAnnotationRemapper.java b/src/main/java/io/papermc/asm/rules/rename/asm/FixedAnnotationRemapper.java similarity index 96% rename from src/main/java/io/papermc/asm/rules/rename/FixedAnnotationRemapper.java rename to src/main/java/io/papermc/asm/rules/rename/asm/FixedAnnotationRemapper.java index 21884c4..927baba 100644 --- a/src/main/java/io/papermc/asm/rules/rename/FixedAnnotationRemapper.java +++ b/src/main/java/io/papermc/asm/rules/rename/asm/FixedAnnotationRemapper.java @@ -1,4 +1,4 @@ -package io.papermc.asm.rules.rename; +package io.papermc.asm.rules.rename.asm; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Type; diff --git a/src/main/java/io/papermc/asm/rules/rename/FixedClassRemapper.java b/src/main/java/io/papermc/asm/rules/rename/asm/FixedClassRemapper.java similarity index 97% rename from src/main/java/io/papermc/asm/rules/rename/FixedClassRemapper.java rename to src/main/java/io/papermc/asm/rules/rename/asm/FixedClassRemapper.java index 3522773..a5c5484 100644 --- a/src/main/java/io/papermc/asm/rules/rename/FixedClassRemapper.java +++ b/src/main/java/io/papermc/asm/rules/rename/asm/FixedClassRemapper.java @@ -1,4 +1,4 @@ -package io.papermc.asm.rules.rename; +package io.papermc.asm.rules.rename.asm; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; diff --git a/src/main/java/io/papermc/asm/rules/rename/FixedFieldRemapper.java b/src/main/java/io/papermc/asm/rules/rename/asm/FixedFieldRemapper.java similarity index 93% rename from src/main/java/io/papermc/asm/rules/rename/FixedFieldRemapper.java rename to src/main/java/io/papermc/asm/rules/rename/asm/FixedFieldRemapper.java index fe59165..1dcc298 100644 --- a/src/main/java/io/papermc/asm/rules/rename/FixedFieldRemapper.java +++ b/src/main/java/io/papermc/asm/rules/rename/asm/FixedFieldRemapper.java @@ -1,4 +1,4 @@ -package io.papermc.asm.rules.rename; +package io.papermc.asm.rules.rename.asm; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.FieldVisitor; diff --git a/src/main/java/io/papermc/asm/rules/rename/FixedMethodRemapper.java b/src/main/java/io/papermc/asm/rules/rename/asm/FixedMethodRemapper.java similarity index 94% rename from src/main/java/io/papermc/asm/rules/rename/FixedMethodRemapper.java rename to src/main/java/io/papermc/asm/rules/rename/asm/FixedMethodRemapper.java index 2089e50..fa094b5 100644 --- a/src/main/java/io/papermc/asm/rules/rename/FixedMethodRemapper.java +++ b/src/main/java/io/papermc/asm/rules/rename/asm/FixedMethodRemapper.java @@ -1,4 +1,4 @@ -package io.papermc.asm.rules.rename; +package io.papermc.asm.rules.rename.asm; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.MethodVisitor; diff --git a/src/main/java/io/papermc/asm/rules/rename/FixedRecordComponentRemapper.java b/src/main/java/io/papermc/asm/rules/rename/asm/FixedRecordComponentRemapper.java similarity index 94% rename from src/main/java/io/papermc/asm/rules/rename/FixedRecordComponentRemapper.java rename to src/main/java/io/papermc/asm/rules/rename/asm/FixedRecordComponentRemapper.java index e18851c..1b065aa 100644 --- a/src/main/java/io/papermc/asm/rules/rename/FixedRecordComponentRemapper.java +++ b/src/main/java/io/papermc/asm/rules/rename/asm/FixedRecordComponentRemapper.java @@ -1,4 +1,4 @@ -package io.papermc.asm.rules.rename; +package io.papermc.asm.rules.rename.asm; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.RecordComponentVisitor; diff --git a/src/main/java/io/papermc/asm/rules/rename/asm/package-info.java b/src/main/java/io/papermc/asm/rules/rename/asm/package-info.java new file mode 100644 index 0000000..453d695 --- /dev/null +++ b/src/main/java/io/papermc/asm/rules/rename/asm/package-info.java @@ -0,0 +1,5 @@ +@DefaultQualifier(NonNull.class) +package io.papermc.asm.rules.rename.asm; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; diff --git a/src/main/java/io/papermc/asm/versioned/ApiVersion.java b/src/main/java/io/papermc/asm/versioned/ApiVersion.java new file mode 100644 index 0000000..439b25b --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/ApiVersion.java @@ -0,0 +1,23 @@ +package io.papermc.asm.versioned; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.OverrideOnly +public interface ApiVersion extends Comparable { + + default boolean isNewerThan(final ApiVersion apiVersion) { + return this.compareTo(apiVersion) > 0; + } + + default boolean isOlderThan(final ApiVersion apiVersion) { + return this.compareTo(apiVersion) < 0; + } + + default boolean isNewerThanOrSameAs(final ApiVersion apiVersion) { + return this.compareTo(apiVersion) >= 0; + } + + default boolean isOlderThanOrSameAs(final ApiVersion apiVersion) { + return this.compareTo(apiVersion) <= 0; + } +} diff --git a/src/main/java/io/papermc/asm/versioned/CachingVersionedRuleFactory.java b/src/main/java/io/papermc/asm/versioned/CachingVersionedRuleFactory.java new file mode 100644 index 0000000..e0411d9 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/CachingVersionedRuleFactory.java @@ -0,0 +1,31 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.rules.RewriteRule; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.jetbrains.annotations.ApiStatus; + +/** + * Caches creating {@link RewriteRule}s for each {@link ApiVersion}. + */ +public abstract class CachingVersionedRuleFactory implements VersionedRuleFactory { + + private final Map cache = new ConcurrentHashMap<>(); + private @MonotonicNonNull VersionedRuleFactory rootFactory; + + @ApiStatus.OverrideOnly + public abstract VersionedRuleFactory createRootFactory(); + + protected final VersionedRuleFactory rootFactory() { + if (this.rootFactory == null) { + this.rootFactory = this.createRootFactory(); + } + return this.rootFactory; + } + + @Override + public final RewriteRule createRule(final ApiVersion apiVersion) { + return this.cache.computeIfAbsent(apiVersion, this.rootFactory()::createRule); + } +} diff --git a/src/main/java/io/papermc/asm/versioned/MappedVersionRuleFactory.java b/src/main/java/io/papermc/asm/versioned/MappedVersionRuleFactory.java new file mode 100644 index 0000000..5efa8a5 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/MappedVersionRuleFactory.java @@ -0,0 +1,27 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.rules.RewriteRule; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.NavigableMap; +import java.util.function.BinaryOperator; + +public record MappedVersionRuleFactory(NavigableMap versions, BinaryOperator mergeFunction) implements VersionedRuleFactory { + + public static > MappedVersionRuleFactory mergeable(final NavigableMap versions) { + return new MappedVersionRuleFactory<>(versions, Mergeable::merge); + } + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + final List toMerge = new ArrayList<>(this.versions.tailMap(apiVersion, true).values()); + if (toMerge.isEmpty()) { + return RewriteRule.EMPTY; + } else if (toMerge.size() == 1) { + return toMerge.get(0); + } + Collections.reverse(toMerge); + return toMerge.stream().reduce(this.mergeFunction).orElseThrow(); + } +} diff --git a/src/main/java/io/papermc/asm/versioned/Mergeable.java b/src/main/java/io/papermc/asm/versioned/Mergeable.java new file mode 100644 index 0000000..c237d28 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/Mergeable.java @@ -0,0 +1,8 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.rules.RewriteRule; + +public interface Mergeable { + + R merge(R other); +} diff --git a/src/main/java/io/papermc/asm/versioned/OwnedVersionedRuleFactoryFactory.java b/src/main/java/io/papermc/asm/versioned/OwnedVersionedRuleFactoryFactory.java new file mode 100644 index 0000000..6270cc3 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/OwnedVersionedRuleFactoryFactory.java @@ -0,0 +1,158 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.rules.builder.matcher.field.FieldMatcher; +import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import io.papermc.asm.versioned.matcher.TargetedMethodMatcherWithHandler; +import io.papermc.asm.versioned.matcher.VersionedMatcher; +import java.lang.constant.ClassDesc; +import java.lang.reflect.Method; +import java.util.NavigableMap; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static io.papermc.asm.util.DescriptorUtils.desc; + +public interface OwnedVersionedRuleFactoryFactory { + + static OwnedVersionedRuleFactoryFactory create(final Set owners) { + return new OwnedVersionedRuleFactoryFactoryImpl(owners); + } + + // + default void plainStaticRewrite(final ClassDesc newOwner, final @Nullable String staticMethodName, final ApiVersion apiVersion, final MethodMatcher matcher) { + this.plainStaticRewrite(newOwner, staticMethodName, VersionedMatcher.single(apiVersion, matcher)); + } + + void plainStaticRewrite(ClassDesc newOwner, @Nullable String staticMethodName, VersionedMatcher versions); + // + + // + default void changeParamToSuper(final Class newParamType, final ApiVersion apiVersion, final TargetedMethodMatcher methodMatcher) { + this.changeParamToSuper(desc(newParamType), apiVersion, methodMatcher); + } + + default void changeParamToSuper(final ClassDesc newParamType, final ApiVersion apiVersion, final TargetedMethodMatcher methodMatcher) { + this.changeParamToSuper(newParamType, VersionedMatcher.single(apiVersion, methodMatcher)); + } + + default void changeParamToSuper(final Class newParamType, final VersionedMatcher versions) { + this.changeParamToSuper(desc(newParamType), versions); + } + + void changeParamToSuper(ClassDesc newParamType, VersionedMatcher versions); + // + + // + default void changeParamFuzzy(final Class newParamType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher targetedMethodMatcher) { + this.changeParamFuzzy(desc(newParamType), apiVersion, staticHandler, targetedMethodMatcher); + } + + default void changeParamFuzzy(final ClassDesc newParamType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher targetedMethodMatcher) { + this.changeParamFuzzy(newParamType, VersionedMatcher.single(apiVersion, new TargetedMethodMatcherWithHandler(targetedMethodMatcher, staticHandler))); + } + + default void changeParamFuzzy(final Class newParamType, final VersionedMatcher versions) { + this.changeParamFuzzy(desc(newParamType), versions); + } + + void changeParamFuzzy(ClassDesc newParamType, VersionedMatcher versions); + // + + // + default void changeParamDirect(final Class newParamType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher methodMatcher) { + this.changeParamDirect(desc(newParamType), apiVersion, staticHandler, methodMatcher); + } + + default void changeParamDirect(final ClassDesc newParamType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher methodMatcher) { + this.changeParamDirect(newParamType, VersionedMatcher.single(apiVersion, new TargetedMethodMatcherWithHandler(methodMatcher, staticHandler))); + } + + default void changeParamDirect(final Class newParamType, final VersionedMatcher versions) { + this.changeParamDirect(desc(newParamType), versions); + } + + void changeParamDirect(ClassDesc newParamType, VersionedMatcher versions); + // + + // + default void changeReturnTypeToSub(final Class newReturnType, final ApiVersion apiVersion, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeToSub(desc(newReturnType), apiVersion, methodMatcher); + } + + default void changeReturnTypeToSub(final ClassDesc newReturnType, final ApiVersion apiVersion, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeToSub(newReturnType, VersionedMatcher.single(apiVersion, methodMatcher)); + } + + default void changeReturnTypeToSub(final Class newReturnType, final VersionedMatcher versions) { + this.changeReturnTypeToSub(desc(newReturnType), versions); + } + + void changeReturnTypeToSub(ClassDesc newReturnType, VersionedMatcher versions); + // + + // + default void changeReturnTypeDirect(final Class newReturnType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeDirect(desc(newReturnType), apiVersion, staticHandler, methodMatcher); + } + + default void changeReturnTypeDirect(final ClassDesc newReturnType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeDirect(newReturnType, VersionedMatcher.single(apiVersion, new TargetedMethodMatcherWithHandler(methodMatcher, staticHandler))); + } + + default void changeReturnTypeDirect(final Class newReturnType, final VersionedMatcher versions) { + this.changeReturnTypeDirect(desc(newReturnType), versions); + } + + void changeReturnTypeDirect(ClassDesc newReturnType, VersionedMatcher versions); + // + + // + default void changeReturnTypeDirectWithContext(final Class newReturnType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeDirectWithContext(desc(newReturnType), apiVersion, staticHandler, methodMatcher); + } + + default void changeReturnTypeDirectWithContext(final ClassDesc newReturnType, final ApiVersion apiVersion, final Method staticHandler, final TargetedMethodMatcher methodMatcher) { + this.changeReturnTypeDirectWithContext(newReturnType, VersionedMatcher.single(apiVersion, new TargetedMethodMatcherWithHandler(methodMatcher, staticHandler))); + } + + default void changeReturnTypeDirectWithContext(final Class newReturnType, final VersionedMatcher versions) { + this.changeReturnTypeDirectWithContext(desc(newReturnType), versions); + } + + void changeReturnTypeDirectWithContext(ClassDesc newReturnType, VersionedMatcher versions); + // + + // + default void changeFieldToMethod(final @Nullable String getterName, final @Nullable String setterName, final boolean isInterfaceMethod, final ApiVersion apiVersion, final FieldMatcher matcher) { + this.changeFieldToMethod(getterName, setterName, isInterfaceMethod, VersionedMatcher.single(apiVersion, matcher)); + } + + void changeFieldToMethod(@Nullable String getterName, @Nullable String setterName, boolean isInterfaceMethod, VersionedMatcher versions); + // + + // + default void moveInstanceMethod(final Class newOwner, final String newMethodName, final ApiVersion apiVersion, final MethodMatcher matcher) { + this.moveInstanceMethod(desc(newOwner), newMethodName, apiVersion, matcher); + } + + default void moveInstanceMethod(final ClassDesc newOwner, final String newMethodName, final ApiVersion apiVersion, final MethodMatcher matcher) { + this.moveInstanceMethod(newOwner, newMethodName, VersionedMatcher.single(apiVersion, matcher)); + } + + default void moveInstanceMethod(final Class newOwner, final String newMethodName, final VersionedMatcher versions) { + this.moveInstanceMethod(desc(newOwner), newMethodName, versions); + } + + void moveInstanceMethod(ClassDesc newOwner, String newMethodName, VersionedMatcher versions); + // + + > void addMergeableRuleFactory(NavigableMap versions); + + void addChainableRuleFactory(NavigableMap versions); + + void addRuleFactory(VersionedRuleFactory factory); + + VersionedRuleFactory build(); +} diff --git a/src/main/java/io/papermc/asm/versioned/OwnedVersionedRuleFactoryFactoryImpl.java b/src/main/java/io/papermc/asm/versioned/OwnedVersionedRuleFactoryFactoryImpl.java new file mode 100644 index 0000000..9ec81a0 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/OwnedVersionedRuleFactoryFactoryImpl.java @@ -0,0 +1,97 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.rules.builder.matcher.field.FieldMatcher; +import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import io.papermc.asm.rules.field.FieldToMethodRewrite; +import io.papermc.asm.rules.method.DirectStaticRewrite; +import io.papermc.asm.rules.method.MoveInstanceMethod; +import io.papermc.asm.rules.method.params.DirectParameterRewrite; +import io.papermc.asm.rules.method.params.FuzzyParameterRewrite; +import io.papermc.asm.rules.method.params.SuperTypeParamRewrite; +import io.papermc.asm.rules.method.returns.DirectReturnRewrite; +import io.papermc.asm.rules.method.returns.SubTypeReturnRewrite; +import io.papermc.asm.versioned.matcher.TargetedMethodMatcherWithHandler; +import io.papermc.asm.versioned.matcher.VersionedMatcher; +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableMap; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class OwnedVersionedRuleFactoryFactoryImpl implements OwnedVersionedRuleFactoryFactory { + + final Set owners; + private final List factories = new ArrayList<>(); + + public OwnedVersionedRuleFactoryFactoryImpl(final Set owners) { + this.owners = Set.copyOf(owners); + } + + @Override + public void plainStaticRewrite(final ClassDesc newOwner, final @Nullable String staticMethodName, final VersionedMatcher versions) { + this.factories.add(new DirectStaticRewrite.Versioned(this.owners, newOwner, staticMethodName, versions)); + } + + @Override + public void changeParamToSuper(final ClassDesc newParamType, final VersionedMatcher versions) { + this.factories.add(new SuperTypeParamRewrite.Versioned(this.owners, newParamType, versions)); + } + + @Override + public void changeParamFuzzy(final ClassDesc newParamType, final VersionedMatcher versions) { + this.factories.add(new FuzzyParameterRewrite.Versioned(this.owners, newParamType, versions)); + } + + @Override + public void changeParamDirect(final ClassDesc newParamType, final VersionedMatcher versions) { + this.factories.add(new DirectParameterRewrite.Versioned(this.owners, newParamType, versions)); + } + + @Override + public void changeReturnTypeToSub(final ClassDesc newReturnType, final VersionedMatcher versions) { + this.factories.add(new SubTypeReturnRewrite.Versioned(this.owners, newReturnType, versions)); + } + + @Override + public void changeReturnTypeDirect(final ClassDesc newReturnType, final VersionedMatcher versions) { + this.factories.add(new DirectReturnRewrite.Versioned(this.owners, newReturnType, versions, false)); + } + + @Override + public void changeReturnTypeDirectWithContext(final ClassDesc newReturnType, final VersionedMatcher versions) { + this.factories.add(new DirectReturnRewrite.Versioned(this.owners, newReturnType, versions, true)); + } + + @Override + public void changeFieldToMethod(final @Nullable String getterName, final @Nullable String setterName, final boolean isInterfaceMethod, final VersionedMatcher versions) { + this.factories.add(new FieldToMethodRewrite.Versioned(this.owners, getterName, setterName, isInterfaceMethod, versions)); + } + + @Override + public void moveInstanceMethod(final ClassDesc newOwner, final String newMethodName, final VersionedMatcher versions) { + this.factories.add(new MoveInstanceMethod.Versioned(this.owners, newOwner, newMethodName, versions)); + } + + @Override + public > void addMergeableRuleFactory(final NavigableMap versions) { + this.factories.add(new MappedVersionRuleFactory<>(versions, Mergeable::merge)); + } + + @Override + public void addChainableRuleFactory(final NavigableMap versions) { + this.factories.add(new MappedVersionRuleFactory(versions, RewriteRule::chain)); + } + + @Override + public void addRuleFactory(final VersionedRuleFactory factory) { + this.factories.add(factory); + } + + @Override + public VersionedRuleFactory build() { + return VersionedRuleFactory.chain(this.factories); + } +} diff --git a/src/main/java/io/papermc/asm/versioned/VersionedRuleFactory.java b/src/main/java/io/papermc/asm/versioned/VersionedRuleFactory.java new file mode 100644 index 0000000..b311223 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/VersionedRuleFactory.java @@ -0,0 +1,82 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.util.DescriptorUtils; +import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A factory for {@link RewriteRule} that are determined + * by a {@link ApiVersion}. + */ +public interface VersionedRuleFactory { + + VersionedRuleFactory EMPTY = apiVersion -> RewriteRule.EMPTY; + + @SafeVarargs + static VersionedRuleFactory forOwnerClass(final Class owner, final Consumer firstFactoryConsumer, final Consumer... factoryConsumers) { + return forOwnerClasses(Collections.singleton(owner), firstFactoryConsumer, factoryConsumers); + } + + @SafeVarargs + static VersionedRuleFactory forOwnerClasses(final Set> owners, final Consumer firstFactoryConsumer, final Consumer... factoryConsumers) { + return forOwners(owners.stream().map(DescriptorUtils::desc).collect(Collectors.toUnmodifiableSet()), firstFactoryConsumer, factoryConsumers); + } + + @SafeVarargs + static VersionedRuleFactory forOwner(final ClassDesc owner, final Consumer firstFactoryConsumer, final Consumer... factoryConsumers) { + return forOwners(Collections.singleton(owner), firstFactoryConsumer, factoryConsumers); + } + + @SafeVarargs + static VersionedRuleFactory forOwners(final Set owners, final Consumer firstFactoryConsumer, final Consumer... factoryConsumers) { + final OwnedVersionedRuleFactoryFactory factory = OwnedVersionedRuleFactoryFactory.create(owners); + firstFactoryConsumer.accept(factory); + for (final Consumer factoryConsumer : factoryConsumers) { + factoryConsumer.accept(factory); + } + return factory.build(); + } + + static VersionedRuleFactory chain(final VersionedRuleFactory... factories) { + return chain(Arrays.asList(factories)); + } + + static VersionedRuleFactory chain(final Collection factories) { + if (factories.isEmpty()) { + return EMPTY; + } else if (factories.size() == 1) { + return factories.iterator().next(); + } + return new Chain(List.copyOf(factories)); + } + + RewriteRule createRule(ApiVersion apiVersion); + + record Chain(List factories) implements VersionedRuleFactory { + + public Chain { + factories = List.copyOf(factories); + } + + @Override + public RewriteRule createRule(final ApiVersion apiVersion) { + final List rules = new ArrayList<>(); + for (final VersionedRuleFactory factory : this.factories) { + final @Nullable RewriteRule rule = factory.createRule(apiVersion); + if (rule != RewriteRule.EMPTY) { + rules.add(rule); + } + } + return RewriteRule.chain(rules); + } + } +} diff --git a/src/main/java/io/papermc/asm/versioned/matcher/TargetedMethodMatcherWithHandler.java b/src/main/java/io/papermc/asm/versioned/matcher/TargetedMethodMatcherWithHandler.java new file mode 100644 index 0000000..1cea5ae --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/matcher/TargetedMethodMatcherWithHandler.java @@ -0,0 +1,7 @@ +package io.papermc.asm.versioned.matcher; + +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import java.lang.reflect.Method; + +public record TargetedMethodMatcherWithHandler(TargetedMethodMatcher matcher, Method staticHandler) { +} diff --git a/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcher.java b/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcher.java new file mode 100644 index 0000000..5eb198b --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcher.java @@ -0,0 +1,53 @@ +package io.papermc.asm.versioned.matcher; + +import io.papermc.asm.rules.RewriteRule; +import io.papermc.asm.rules.builder.matcher.field.FieldMatcher; +import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import io.papermc.asm.versioned.ApiVersion; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class VersionedMatcher { + + public static VersionedMatcher single(final ApiVersion apiVersion, final C context) { + return new VersionedMatcher<>(new TreeMap<>(Map.of(apiVersion, context))); + } + + public static VersionedMatcherBuilder fieldBuilder() { + return builder(); + } + + public static VersionedMatcherBuilder methodBuilder() { + return builder(); + } + + public static VersionedMatcherBuilder targetedMethodBuilder() { + return builder(); + } + + public static VersionedMatcherBuilder builder() { + return new VersionedMatcherBuilderImpl<>(); + } + + private final NavigableMap map; + + public VersionedMatcher(final NavigableMap map) { + this.map = map; + } + + public RewriteRule ruleForVersion(final ApiVersion version, final Function creator) { + return ruleForVersion(this.map, version, creator); + } + + public static

RewriteRule ruleForVersion(final NavigableMap versions, final ApiVersion version, final Function creator) { + final Map.@Nullable Entry entry = versions.ceilingEntry(version); + if (entry == null) { + return RewriteRule.EMPTY; + } + return creator.apply(entry.getValue()); + } +} diff --git a/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcherBuilder.java b/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcherBuilder.java new file mode 100644 index 0000000..cc6022d --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcherBuilder.java @@ -0,0 +1,11 @@ +package io.papermc.asm.versioned.matcher; + +import io.papermc.asm.util.Builder; +import io.papermc.asm.versioned.ApiVersion; +import org.jetbrains.annotations.Contract; + +public interface VersionedMatcherBuilder extends Builder> { + + @Contract(value = "_, _ -> this", mutates = "this") + VersionedMatcherBuilder with(ApiVersion apiVersion, C context); +} diff --git a/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcherBuilderImpl.java b/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcherBuilderImpl.java new file mode 100644 index 0000000..3aacff6 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/matcher/VersionedMatcherBuilderImpl.java @@ -0,0 +1,24 @@ +package io.papermc.asm.versioned.matcher; + +import io.papermc.asm.versioned.ApiVersion; +import java.util.NavigableMap; +import java.util.TreeMap; + +public class VersionedMatcherBuilderImpl implements VersionedMatcherBuilder { + + protected final NavigableMap versions = new TreeMap<>(); + + @Override + public VersionedMatcherBuilder with(final ApiVersion apiVersion, final C context) { + if (this.versions.containsKey(apiVersion)) { + throw new IllegalArgumentException("Duplicate version: " + apiVersion); + } + this.versions.put(apiVersion, context); + return this; + } + + @Override + public VersionedMatcher build() { + return new VersionedMatcher<>(this.versions); + } +} diff --git a/src/main/java/io/papermc/asm/versioned/matcher/VersionedTargetedMethodMatcherWithHandlerBuilder.java b/src/main/java/io/papermc/asm/versioned/matcher/VersionedTargetedMethodMatcherWithHandlerBuilder.java new file mode 100644 index 0000000..0b2f0c2 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/matcher/VersionedTargetedMethodMatcherWithHandlerBuilder.java @@ -0,0 +1,16 @@ +package io.papermc.asm.versioned.matcher; + +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import io.papermc.asm.versioned.ApiVersion; +import java.lang.reflect.Method; +import org.jetbrains.annotations.Contract; + +public interface VersionedTargetedMethodMatcherWithHandlerBuilder extends VersionedMatcherBuilder { + + @Contract(value = "_, _, _ -> this", mutates = "this") + VersionedTargetedMethodMatcherWithHandlerBuilder with(ApiVersion apiVersion, TargetedMethodMatcher matcher, Method staticHandler); + + @Override + VersionedTargetedMethodMatcherWithHandlerBuilder with(ApiVersion apiVersion, TargetedMethodMatcherWithHandler context); + +} diff --git a/src/main/java/io/papermc/asm/versioned/matcher/VersionedTargetedMethodMatcherWithHandlerBuilderImpl.java b/src/main/java/io/papermc/asm/versioned/matcher/VersionedTargetedMethodMatcherWithHandlerBuilderImpl.java new file mode 100644 index 0000000..28d1c97 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/matcher/VersionedTargetedMethodMatcherWithHandlerBuilderImpl.java @@ -0,0 +1,20 @@ +package io.papermc.asm.versioned.matcher; + +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import io.papermc.asm.versioned.ApiVersion; +import java.lang.reflect.Method; + +public final class VersionedTargetedMethodMatcherWithHandlerBuilderImpl + extends VersionedMatcherBuilderImpl + implements VersionedTargetedMethodMatcherWithHandlerBuilder { + + @Override + public VersionedTargetedMethodMatcherWithHandlerBuilder with(final ApiVersion apiVersion, final TargetedMethodMatcherWithHandler context) { + return (VersionedTargetedMethodMatcherWithHandlerBuilder) super.with(apiVersion, context); + } + + @Override + public VersionedTargetedMethodMatcherWithHandlerBuilder with(final ApiVersion apiVersion, final TargetedMethodMatcher matcher, final Method staticHandler) { + return this.with(apiVersion, new TargetedMethodMatcherWithHandler(matcher, staticHandler)); + } +} diff --git a/src/main/java/io/papermc/asm/versioned/package-info.java b/src/main/java/io/papermc/asm/versioned/package-info.java new file mode 100644 index 0000000..6e8e9a3 --- /dev/null +++ b/src/main/java/io/papermc/asm/versioned/package-info.java @@ -0,0 +1,5 @@ +@DefaultQualifier(NonNull.class) +package io.papermc.asm.versioned; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; diff --git a/src/test/java/io/papermc/asm/ApiVersion.java b/src/test/java/io/papermc/asm/ApiVersion.java new file mode 100644 index 0000000..f9067c7 --- /dev/null +++ b/src/test/java/io/papermc/asm/ApiVersion.java @@ -0,0 +1,21 @@ +package io.papermc.asm; + +import java.util.List; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public record ApiVersion(int version) implements io.papermc.asm.versioned.ApiVersion { + + public static final ApiVersion ONE = new ApiVersion(1); + public static final ApiVersion TWO = new ApiVersion(2); + public static final ApiVersion THREE = new ApiVersion(3); + public static final ApiVersion FOUR = new ApiVersion(4); + + public static final List ALL_VERSIONS = List.of(ONE, TWO, THREE, FOUR); + + @Override + public int compareTo(final io.papermc.asm.versioned.ApiVersion o) { + return Integer.compare(this.version, ((ApiVersion) o).version); + } +} diff --git a/src/test/java/io/papermc/asm/rules/methods/MethodRewritesTest.java b/src/test/java/io/papermc/asm/rules/methods/MethodRewritesTest.java index 68c7dde..7b3c37c 100644 --- a/src/test/java/io/papermc/asm/rules/methods/MethodRewritesTest.java +++ b/src/test/java/io/papermc/asm/rules/methods/MethodRewritesTest.java @@ -3,12 +3,21 @@ import data.methods.Methods; import data.types.hierarchy.Entity; import data.types.hierarchy.Player; +import io.papermc.asm.ApiVersion; import io.papermc.asm.TransformerTest; import io.papermc.asm.checks.TransformerCheck; import io.papermc.asm.rules.RewriteRule; -import io.papermc.asm.rules.builder.matcher.method.MethodMatcher; -import io.papermc.asm.rules.builder.matcher.method.MethodMatcherBuilder; +import io.papermc.asm.rules.builder.matcher.method.MethodTypeMatcherBuilder; +import io.papermc.asm.rules.builder.matcher.method.targeted.TargetedMethodMatcher; +import io.papermc.asm.rules.method.params.SuperTypeParamRewrite; +import io.papermc.asm.rules.method.returns.SubTypeReturnRewrite; +import io.papermc.asm.versioned.VersionedRuleFactory; +import io.papermc.asm.versioned.VersionedTester; +import io.papermc.asm.versioned.matcher.VersionedMatcher; import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.util.Map; +import org.junit.jupiter.api.Test; class MethodRewritesTest { @@ -20,31 +29,75 @@ class MethodRewritesTest { void testSuperTypeParam(final TransformerCheck check) { final RewriteRule rule = RewriteRule.forOwnerClass(Methods.class, builder -> { builder.changeParamToSuper( - Player.class, Entity.class, - MethodMatcher.builder() + TargetedMethodMatcher.builder() .match("consume", b -> b.virtual()) .match("consumeStatic", b -> b.statik()) - .hasParam(PLAYER) + .targetParam(PLAYER) .build() ); }); check.run(rule); } + @Test + void testVersionedSuperTypeParam() { + final VersionedRuleFactory factory = VersionedRuleFactory.forOwnerClass(String.class, builder -> { + final TargetedMethodMatcher method1 = TargetedMethodMatcher.builder() + .match("method1").targetParam(ConstantDescs.CD_int).build(); + final TargetedMethodMatcher method3 = TargetedMethodMatcher.builder() + .match("method1").targetParam(ConstantDescs.CD_long).build(); + builder.changeParamToSuper( + String.class, + VersionedMatcher.targetedMethodBuilder() + .with(ApiVersion.ONE, method1) + .with(ApiVersion.THREE, method3) + .build() + ); + }); + + final VersionedTester tester = new VersionedTester(factory, ApiVersion.ALL_VERSIONS); + tester.test(SuperTypeParamRewrite::oldParamType, Map.of( + ApiVersion.ONE, ConstantDescs.CD_int, + ApiVersion.THREE, ConstantDescs.CD_long + )); + } + @TransformerTest("data.methods.inplace.SubTypeReturnUser") void testSubTypeReturn(final TransformerCheck check) { final RewriteRule rule = RewriteRule.forOwnerClass(Methods.class, builder -> { builder.changeReturnTypeToSub( - Entity.class, Player.class, - MethodMatcher.builder() - .match("get", MethodMatcherBuilder.MatchBuilder::virtual) - .match("getStatic", MethodMatcherBuilder.MatchBuilder::statik) - .hasReturn(ENTITY) + TargetedMethodMatcher.builder() + .match("get", MethodTypeMatcherBuilder::virtual) + .match("getStatic", MethodTypeMatcherBuilder::statik) + .targetReturn(ENTITY) .build() ); }); check.run(rule); } + + @Test + void testVersionedSubTypeReturn() { + final VersionedRuleFactory factory = VersionedRuleFactory.forOwnerClass(String.class, builder -> { + final TargetedMethodMatcher method1 = TargetedMethodMatcher.builder() + .match("method1").targetReturn(ConstantDescs.CD_int).build(); + final TargetedMethodMatcher method3 = TargetedMethodMatcher.builder() + .match("method1").targetReturn(ConstantDescs.CD_long).build(); + builder.changeReturnTypeToSub( + String.class, + VersionedMatcher.targetedMethodBuilder() + .with(ApiVersion.ONE, method1) + .with(ApiVersion.THREE, method3) + .build() + ); + }); + + final VersionedTester tester = new VersionedTester(factory, ApiVersion.ALL_VERSIONS); + tester.test(SubTypeReturnRewrite::oldReturnType, Map.of( + ApiVersion.ONE, ConstantDescs.CD_int, + ApiVersion.THREE, ConstantDescs.CD_long + )); + } } diff --git a/src/test/java/io/papermc/asm/rules/rename/RenameRuleTest.java b/src/test/java/io/papermc/asm/rules/rename/RenameRuleTest.java index 80ee3b8..297e3e3 100644 --- a/src/test/java/io/papermc/asm/rules/rename/RenameRuleTest.java +++ b/src/test/java/io/papermc/asm/rules/rename/RenameRuleTest.java @@ -1,21 +1,91 @@ package io.papermc.asm.rules.rename; +import data.types.rename.RenamedTestEnum; +import data.types.rename.TestAnnotation; +import io.papermc.asm.ApiVersion; import io.papermc.asm.TransformerTest; import io.papermc.asm.checks.TransformerCheck; +import io.papermc.asm.versioned.MappedVersionRuleFactory; +import io.papermc.asm.versioned.VersionedRuleFactory; +import java.lang.constant.ClassDesc; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import static io.papermc.asm.util.DescriptorUtils.fromOwner; +import static io.papermc.asm.util.DescriptorUtils.methodDesc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; class RenameRuleTest { + private static final ClassDesc TEST_ENUM = fromOwner("data/types/rename/TestEnum"); + @TransformerTest("data.rename.RenameTest") - void testAnnotationSpecificRenames(final TransformerCheck check) { + void testRenamerRule(final TransformerCheck check) { final RenameRule rule = RenameRule.builder() - .type("data/types/rename/TestEnum", "data/types/rename/RenamedTestEnum") - .fieldByInternal("data/types/rename/TestEnum", "A", "ONE") - .fieldByInternal("data/types/rename/TestEnum", "B", "TWO") - .fieldByInternal("data/types/rename/TestEnum", "C", "THREE") - .annotationAttribute("Ldata/types/rename/TestAnnotation;", "single", "value") - .methodByInternal("data/types/rename/TestAnnotation", "single", "()Ldata/types/rename/TestEnum;", "value") + .type("data/types/rename/TestEnum", RenamedTestEnum.class) + .editEnum(TEST_ENUM, builder -> { + builder + .rename("A", "ONE") + .rename("B", "TWO") + .rename("C", "THREE") + .rename("FB", "FOUR") + .rename("Ea", "FIVE"); + }) + .annotationAttribute(TestAnnotation.class, "single", "value") + .methodByClass(TestAnnotation.class, "single", methodDesc("()Ldata/types/rename/TestEnum;"), "value") .build(); check.run(rule); } + + @Test + void testVersionedRenamerRule() { + final Map versions = new HashMap<>(); + versions.put(ApiVersion.ONE, RenameRule.builder() + .methodByClass(TestAnnotation.class, "single", methodDesc("()Ldata/types/rename/TestEnum;"), "value") + .editEnum(TEST_ENUM, builder -> builder + .rename("A", "ONE") + ) + .build() + ); + versions.put(ApiVersion.THREE, RenameRule.builder() + .methodByClass(TestAnnotation.class, "newValue", methodDesc("()Ldata/types/rename/TestEnum;"), "value") + .annotationAttribute(TestAnnotation.class, "newValue", "value") + .editEnum(TEST_ENUM, builder -> builder + .rename("OTHER_A", "ONE") + .rename("B", "TWO") + ) + .build() + ); + + final VersionedRuleFactory factory = MappedVersionRuleFactory.mergeable(new TreeMap<>(versions)); + final RenameRule ruleOne = (RenameRule) factory.createRule(ApiVersion.ONE); + final RenameRule ruleTwo = (RenameRule) factory.createRule(ApiVersion.TWO); + assertEquals("value", annotationMethod("single").apply(ruleOne)); + assertEquals("value", annotationMethod("newValue").apply(ruleOne)); + + assertNull(annotationMethod("single").apply(ruleTwo)); + assertEquals("value", annotationMethod("newValue").apply(ruleTwo)); + + assertEquals("ONE", enumField("A").apply(ruleOne)); + assertEquals("ONE", enumField("OTHER_A").apply(ruleOne)); + assertEquals("TWO", enumField("B").apply(ruleOne)); + + assertEquals("ONE", enumField("OTHER_A").apply(ruleTwo)); + assertNull(enumField("A").apply(ruleTwo)); + assertEquals("TWO", enumField("B").apply(ruleTwo)); + } + + private static Function annotationMethod(final String legacyName) { + return renameRule -> renameRule.renames().get("%s.%s%s".formatted("data/types/rename/TestAnnotation", legacyName, "()Ldata/types/rename/TestEnum;")); + } + + private static Function enumField(final String legacyName) { + return renameRule -> renameRule.enumFieldRenames().get(TEST_ENUM).fieldRenames().get(legacyName); + } } diff --git a/src/test/java/io/papermc/asm/versioned/VersionedTester.java b/src/test/java/io/papermc/asm/versioned/VersionedTester.java new file mode 100644 index 0000000..f2ced83 --- /dev/null +++ b/src/test/java/io/papermc/asm/versioned/VersionedTester.java @@ -0,0 +1,46 @@ +package io.papermc.asm.versioned; + +import io.papermc.asm.ApiVersion; +import io.papermc.asm.rules.RewriteRule; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class VersionedTester { + + private final VersionedRuleFactory factory; + private final List versions; + + public VersionedTester(final VersionedRuleFactory factory, final List versions) { + this.factory = factory; + this.versions = versions; + } + + @SuppressWarnings("unchecked") + public void test(final Function comparisonGetter, final Map expectedValues) { + final Map sortedExpectedValues = new TreeMap<>(expectedValues); + if (sortedExpectedValues.size() + 1 > this.versions.size()) { + throw new IllegalArgumentException("Expected values size does not match versions size"); + } + final Iterator> expectedEntryIter = sortedExpectedValues.entrySet().iterator(); + Map.Entry current = expectedEntryIter.next(); + for (int i = 0; i < this.versions.size() - 1; i++) { + final ApiVersion version = this.versions.get(i); + final R rule = (R) this.factory.createRule(version); + if (version.isNewerThan(current.getKey())) { + current = expectedEntryIter.next(); + } + final C expected = current.getValue(); + final C actual = comparisonGetter.apply(rule); + assertEquals(expected, actual, "Expected " + expected + " but got " + actual + " for version " + version); + } + assertFalse(expectedEntryIter.hasNext()); + final RewriteRule rule = this.factory.createRule(this.versions.get(this.versions.size() - 1)); + assertEquals(RewriteRule.EMPTY, rule, "Expected empty rule for version " + this.versions.get(this.versions.size() - 1)); + } +} diff --git a/src/testData/java/data/rename/RenameTest.java b/src/testData/java/data/rename/RenameTest.java index 76ce74a..9533019 100644 --- a/src/testData/java/data/rename/RenameTest.java +++ b/src/testData/java/data/rename/RenameTest.java @@ -15,6 +15,13 @@ public static void entry() throws ReflectiveOperationException { 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); } private static void checkAnnotation(final AnnotatedElement element) { diff --git a/src/testData/java/data/types/rename/TestEnum.java b/src/testData/java/data/types/rename/TestEnum.java index ad396a8..0a46d4f 100644 --- a/src/testData/java/data/types/rename/TestEnum.java +++ b/src/testData/java/data/types/rename/TestEnum.java @@ -4,5 +4,7 @@ public enum TestEnum { A, B, C, + FB, // FB and Ea have the same string hashCode + Ea, ; } diff --git a/src/testData/resources/expected/data/rename/RenameTest.class b/src/testData/resources/expected/data/rename/RenameTest.class index 4228c7e..d55c38b 100644 Binary files a/src/testData/resources/expected/data/rename/RenameTest.class and b/src/testData/resources/expected/data/rename/RenameTest.class differ diff --git a/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java b/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java index 34f0b33..ac42c78 100644 --- a/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java +++ b/src/testDataNewTargets/java/data/types/rename/RenamedTestEnum.java @@ -4,5 +4,7 @@ public enum RenamedTestEnum { ONE, TWO, THREE, + FOUR, + FIVE, ; }