Skip to content

Commit 4163ee2

Browse files
committed
Resolve overloaded method target with different name
Fixes Sinytra/Connector#2107
1 parent 869a4bb commit 4163ee2

7 files changed

Lines changed: 110 additions & 28 deletions

File tree

core/src/main/java/org/sinytra/adapter/analysis/method/MethodAnalyzer.java

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77
import org.objectweb.asm.Opcodes;
88
import org.objectweb.asm.tree.*;
99
import org.objectweb.asm.tree.analysis.*;
10+
import org.sinytra.adapter.analysis.selector.FrameUtil;
11+
import org.sinytra.adapter.env.ctx.TargetPair;
1012
import org.sinytra.adapter.env.util.TypeConstants;
1113
import org.sinytra.adapter.util.AdapterUtil;
1214
import org.sinytra.adapter.util.MethodQualifier;
13-
import org.sinytra.adapter.util.OpcodeUtil;
1415

15-
import java.util.ArrayList;
16-
import java.util.Collection;
17-
import java.util.HashSet;
18-
import java.util.List;
16+
import java.util.*;
1917
import java.util.function.BiPredicate;
2018

2119
public class MethodAnalyzer {
@@ -26,32 +24,47 @@ public static boolean isDirtyDeprecatedMethod(MethodNode clean, MethodNode dirty
2624
&& AdapterUtil.hasAnnotation(dirty.visibleAnnotations, TypeConstants.DEPRECATED);
2725
}
2826

29-
@Nullable
30-
public static List<MethodNode> collectMethodInvocations(ClassNode cls, MethodNode mtd) {
31-
// Iterate over isns, leave out first and last elements
32-
// Collect method invocations
33-
// All labels must be finalized by a method invocation to pass
34-
List<MethodNode> invocations = new ArrayList<>();
35-
for (int i = 1; i < mtd.instructions.size(); i++) {
36-
AbstractInsnNode insn = mtd.instructions.get(i);
37-
if (insn instanceof LabelNode) {
38-
AbstractInsnNode previous = insn.getPrevious();
39-
AbstractInsnNode effectivePrevious = previous;
40-
if (OpcodeUtil.isReturnOpcode(previous.getOpcode())) {
41-
effectivePrevious = previous.getPrevious();
27+
public static List<MethodNode> getOwnMethodCalls(TargetPair targetPair) {
28+
MethodNode methodNode = targetPair.methodNode();
29+
Frame<SourceValue>[] frames = FrameUtil.getFrames(methodNode);
30+
31+
Set<MethodInsnNode> consumedCalls = new HashSet<>();
32+
for (AbstractInsnNode insn : methodNode.instructions) {
33+
if (!(insn instanceof MethodInsnNode call)) continue;
34+
35+
int index = methodNode.instructions.indexOf(insn);
36+
Frame<SourceValue> frame = frames[index];
37+
if (frame == null) continue;
38+
39+
int argCount = FrameUtil.getPopCount(call);
40+
int stackTop = frame.getStackSize();
41+
42+
// Look at arguments consumed by this call
43+
for (int i = 0; i < argCount; i++) {
44+
SourceValue arg = frame.getStack(stackTop - 1 - i);
45+
46+
for (AbstractInsnNode producer : arg.insns) {
47+
if (producer instanceof MethodInsnNode producerCall) {
48+
consumedCalls.add(producerCall);
49+
}
4250
}
51+
}
52+
}
4353

44-
if (effectivePrevious instanceof MethodInsnNode methodInsn && methodInsn.owner.equals(cls.name)) {
45-
cls.methods.stream()
46-
.filter(m -> m.name.equals(methodInsn.name) && m.desc.equals(methodInsn.desc))
54+
List<MethodNode> topTierCalls = new ArrayList<>();
55+
ClassNode classNode = targetPair.classNode();
56+
for (AbstractInsnNode insn : methodNode.instructions) {
57+
if (insn instanceof MethodInsnNode call && call.owner.equals(classNode.name)) {
58+
if (!consumedCalls.contains(call)) {
59+
classNode.methods.stream()
60+
.filter(m -> m.name.equals(call.name) && m.desc.equals(call.desc))
4761
.findFirst()
48-
.ifPresent(invocations::add);
49-
} else if (previous == null || !OpcodeUtil.isReturnOpcode(previous.getOpcode())) {
50-
return null;
62+
.ifPresent(topTierCalls::add);
5163
}
5264
}
5365
}
54-
return invocations;
66+
67+
return topTierCalls;
5568
}
5669

5770
public static List<String> findLambdasInMethod(ClassNode cls, MethodNode method, @Nullable Multimap<String, MethodNode> methods) {

core/src/main/java/org/sinytra/adapter/analysis/selector/FrameUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static int getPopCount(AbstractInsnNode insn) {
4242
int args = Type.getArgumentsAndReturnSizes(((MethodInsnNode) insn).desc) >> 2;
4343
boolean isStatic = op == Opcodes.INVOKESTATIC;
4444
// INVOKEDYNAMIC is complex, but usually acts like static for the bootstrap
45-
if (op == org.objectweb.asm.Opcodes.INVOKEDYNAMIC) isStatic = true;
45+
if (op == Opcodes.INVOKEDYNAMIC) isStatic = true;
4646

4747
return isStatic ? args : args + 1;
4848
}

core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitMethodCancellationHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static void handle(MixinContext context, Recipe recipe, MethodNode newTar
3535
MixinClassGenerator generator = context.patchContext().environment().classGenerator();
3636
ClassNode generatedTarget = generator.getOrGenerateMixinClass(context.classNode(), originalClassTarget.name, null);
3737

38-
List<MethodNode> invocations = MethodAnalyzer.collectMethodInvocations(originalClassTarget, originalMethodTarget);
38+
List<MethodNode> invocations = MethodAnalyzer.getOwnMethodCalls(originalTarget);
3939
if (invocations == null) {
4040
return;
4141
}

core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitTargetMethodSubResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private static List<CandidateMethod> locateCandidates(MixinContext context, Targ
6161
return tryFindPartialCandidates(cleanTargetMethod, dirtyTargetClass, dirtyTargetMethod, context);
6262
}
6363

64-
List<MethodNode> invocations = MethodAnalyzer.collectMethodInvocations(dirtyTargetClass, dirtyTargetMethod);
64+
List<MethodNode> invocations = MethodAnalyzer.getOwnMethodCalls(dirtyTarget);
6565
if (invocations == null) {
6666
return List.of();
6767
}

core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodSubResolvers.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.objectweb.asm.tree.ClassNode;
88
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
99
import org.objectweb.asm.tree.MethodNode;
10+
import org.sinytra.adapter.analysis.method.MethodAnalyzer;
1011
import org.sinytra.adapter.env.ctx.MixinContext;
1112
import org.sinytra.adapter.env.ctx.TargetPair;
1213
import org.sinytra.adapter.env.param.Parameters;
@@ -32,6 +33,9 @@ public class TargetMethodSubResolvers {
3233
* DIRTY: <code>Lnet/minecraft/server/level/ServerEntity;sendPairingData(Lnet/minecraft/server/level/ServerPlayer;Lnet/neoforged/neoforge/network/bundle/PacketAndPayloadAcceptor;)V</code>
3334
*/
3435
public static final SubResolver CHANGED_METHOD_PARAMS = (MixinContext context, Recipe recipe) -> {
36+
Configuration overloaded = resolveOverloadedReplacement(context, recipe);
37+
if (overloaded != null) return overloaded;
38+
3539
MethodQualifier cleanQualifier = recipe.clean().getTargetMethod();
3640

3741
Pair<ClassNode, List<MethodNode>> candidates = context.methods().findOwnMethodsByName(context.dirtyLookup(), cleanQualifier);
@@ -76,6 +80,24 @@ public class TargetMethodSubResolvers {
7680
return null;
7781
};
7882

83+
// Resolve overloaded method by call for when the name doesn't match. Original method must be marked @Deprecated
84+
// Example: BoneMealItem#growCrop -> applyBonemeal
85+
@Nullable
86+
private static Configuration resolveOverloadedReplacement(MixinContext context, Recipe recipe) {
87+
TargetPair dirtyTarget = recipe.getNewCleanTarget();
88+
if (dirtyTarget == null || !AdapterUtil.isDeprecated(dirtyTarget.methodNode())) return null;
89+
90+
List<MethodNode> invocations = MethodAnalyzer.getOwnMethodCalls(dirtyTarget);
91+
for (MethodNode invocation : invocations) {
92+
if (context.methods().hasInjectionTargetInsns(new TargetPair(dirtyTarget.classNode(), invocation))) {
93+
return MutableConfiguration.create()
94+
.setTargetMethod(invocation);
95+
}
96+
}
97+
98+
return null;
99+
}
100+
79101
@Nullable
80102
private static Configuration resolveReplacementCandidate(MixinContext context, Recipe recipe, List<MethodNode> methods) {
81103
if (methods.size() == 1) {

test/src/test/java/org/sinytra/adapter/patch/test/mixin/DynamicMixinPatchTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,16 @@ void testAmbigousOverloadedTarget() throws Exception {
468468
);
469469
}
470470

471+
@Test
472+
void testChangedTargetMethod() throws Exception {
473+
assertSameCode(
474+
"org/sinytra/adapter/test/mixin/BoneMealMixin",
475+
"cancelBonemeal",
476+
assertTargetMethod(),
477+
assertInjectionPoint()
478+
);
479+
}
480+
471481
@Override
472482
protected LoadResult load(String className, List<String> allowedMethods) throws Exception {
473483
ClassNode patched = loadClass(className);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.sinytra.adapter.test.mixin;
2+
3+
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
4+
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
5+
import net.minecraft.core.BlockPos;
6+
import net.minecraft.server.level.ServerLevel;
7+
import net.minecraft.util.RandomSource;
8+
import net.minecraft.world.item.BoneMealItem;
9+
import net.minecraft.world.level.block.BonemealableBlock;
10+
import net.minecraft.world.level.block.state.BlockState;
11+
import org.spongepowered.asm.mixin.Mixin;
12+
import org.spongepowered.asm.mixin.injection.At;
13+
14+
@Mixin(BoneMealItem.class)
15+
public class BoneMealMixin {
16+
@WrapOperation(
17+
method = "growCrop",
18+
at = @At(
19+
value = "INVOKE",
20+
target = "Lnet/minecraft/world/level/block/BonemealableBlock;performBonemeal(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"
21+
)
22+
)
23+
private static void cancelBonemeal(BonemealableBlock instance, ServerLevel level, RandomSource randomSource, BlockPos blockPos, BlockState blockState, Operation<Void> original) {
24+
original.call(instance, level, randomSource, blockPos, blockState);
25+
}
26+
27+
@WrapOperation(
28+
method = "applyBonemeal(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/player/Player;)Z",
29+
at = @At(
30+
value = "INVOKE",
31+
target = "Lnet/minecraft/world/level/block/BonemealableBlock;performBonemeal(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"
32+
)
33+
)
34+
private static void cancelBonemealExpected(BonemealableBlock instance, ServerLevel level, RandomSource randomSource, BlockPos blockPos, BlockState blockState, Operation<Void> original) {
35+
original.call(instance, level, randomSource, blockPos, blockState);
36+
}
37+
}

0 commit comments

Comments
 (0)