diff --git a/src/functionalTest/java/codechicken/multipart/test/ASMSuperLookupFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/ASMSuperLookupFunctionalTest.java new file mode 100644 index 0000000..91e8205 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/ASMSuperLookupFunctionalTest.java @@ -0,0 +1,306 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.objectweb.asm.Opcodes.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import codechicken.multipart.asm.ASMMixinCompiler; +import codechicken.multipart.asm.ASMMixinCompiler$; +import codechicken.multipart.asm.ASMMixinCompiler.ClassInfo; +import codechicken.multipart.asm.ASMMixinCompiler.MethodInfo; +import codechicken.multipart.asm.StackAnalyser; +import codechicken.multipart.asm.StackAnalyser.Const; +import codechicken.multipart.asm.StackAnalyser.Load; +import codechicken.multipart.asm.StackAnalyser.Param; +import codechicken.multipart.asm.StackAnalyser.StackEntry; +import codechicken.multipart.asm.StackAnalyser.Store; +import codechicken.multipart.asm.StackAnalyser.This; +import scala.Option; +import scala.Some; +import scala.collection.Iterable; +import scala.collection.mutable.Map; + +class ASMSuperLookupFunctionalTest { + + private static final Type OWNER = Type.getType(Child.class); + + @Test + void rejectsOwnOwnerAndWrongNamesBeforeReadingTheDescriptorOrStack() { + StackAnalyser stack = stack("target"); + stack.m().name = null; + assertFalse(ASMMixinCompiler.getSuper(call(OWNER.getInternalName(), null, null), stack).isDefined()); + stack.m().name = "different"; + assertFalse(ASMMixinCompiler.getSuper(call(null, "target", null), stack).isDefined()); + assertFalse(ASMMixinCompiler.getSuper(call(null, null, null), stack).isDefined()); + stack.m().name = "target"; + assertThrows(NullPointerException.class, () -> ASMMixinCompiler.getSuper(call(null, "target", null), stack)); + assertThrows( + IndexOutOfBoundsException.class, + () -> ASMMixinCompiler.getSuper(call(null, "target", "()I"), stack)); + stack.m().name = null; + assertThrows(NullPointerException.class, () -> ASMMixinCompiler.getSuper(call(null, "target", "()I"), stack)); + } + + @Test + void stripsTheGreedyScalaSuperPrefixButRequiresACharacterBeforeIt() { + for (String name : new String[] { "target", "owner$$super$target", "first$$super$second$$super$target" }) { + MethodInfo result = lookup(name, "target", "()I").get(); + assertEquals(Type.getInternalName(Parent.class), result.owner().name()); + assertEquals("target", result.name()); + } + assertFalse(lookup("$$super$target", "target", "()I").isDefined()); + assertFalse(lookup("prefix$$super$targetSuffix", "target", "()I").isDefined()); + assertFalse(lookup("target", "owner$$super$target", "()I").isDefined()); + } + + @Test + void startsAtTheAnalysedOwnersSuperclassAndSelectsTheExactConcreteNonPrivateSignature() { + assertEquals(Type.getInternalName(Parent.class), lookup("target", "target", "()I").get().owner().name()); + StackAnalyser overload = stack("target"); + overload.push(receiver()); + overload.push(new Const(3, null)); + MethodInfo result = ASMMixinCompiler.getSuper(call("unrelated/Owner", "target", "(I)I"), overload).get(); + assertEquals(Type.getInternalName(Grandparent.class), result.owner().name()); + assertEquals("(I)I", result.desc()); + assertEquals( + Type.getInternalName(Grandparent.class), + lookup("inherited", "inherited", "()I").get().owner().name()); + assertTrue(lookup("protectedTarget", "protectedTarget", "()I").isDefined()); + assertFalse(lookup("hidden", "hidden", "()I").isDefined()); + assertFalse(lookup("abstractTarget", "abstractTarget", "()I").isDefined()); + assertFalse(lookup("childOnly", "childOnly", "()I").isDefined()); + assertFalse(lookup("target", "target", "()J").isDefined()); + StackAnalyser root = new StackAnalyser(Type.getType(Object.class), method("target")); + root.push(receiver()); + assertFalse(ASMMixinCompiler.getSuper(call("unrelated/Owner", "target", "()I"), root).isDefined()); + } + + @Test + void requiresALoadOfThisButDoesNotValidateItsTypeTheInvocationOwnerOrOpcode() { + StackEntry[] rejected = { null, new Const(null, null), new Load(null, null), + new Load(new Param(0, OWNER), null), new Load(new Store(receiver(), null), null) }; + for (StackEntry entry : rejected) { + StackAnalyser stack = stack("target"); + stack.stack().$plus$eq(entry); + assertFalse(ASMMixinCompiler.getSuper(call("unrelated/Owner", "target", "()I"), stack).isDefined()); + } + for (int opcode : new int[] { INVOKESPECIAL, INVOKEVIRTUAL, INVOKESTATIC, INVOKEINTERFACE }) { + StackAnalyser stack = stack("target"); + // The pattern does not read This.owner or the entry's type. + stack.stack().$plus$eq(new Load(new This(null), null)); + MethodInsnNode call = new MethodInsnNode(opcode, null, "target", "()I", opcode == INVOKEINTERFACE); + assertEquals( + Type.getInternalName(Parent.class), + ASMMixinCompiler$.MODULE$.getSuper(call, stack).get().owner().name()); + assertEquals(1, stack.stack().size()); + } + } + + @Test + void retainsArgumentCountIndexingEvenWhenWideValuesOccupyTwoStackSlots() { + StackAnalyser narrow = stack("two"); + Load receiver = receiver(); + narrow.push(receiver); + narrow.push(new Const(1, null)); + narrow.push(new Const(2, null)); + assertTrue(ASMMixinCompiler.getSuper(call("parent", "two", "(II)I"), narrow).isDefined()); + assertEquals(3, narrow.stack().size()); + assertSame(receiver, narrow.peek(2)); + for (Object value : new Object[] { 5L, 2.5D }) { + StackAnalyser wide = stack("wide"); + wide.push(receiver); + Const argument = new Const(value, null); + wide.push(argument); + String desc = value instanceof Long ? "(J)I" : "(D)I"; + assertSame(argument, wide.peek(1)); + assertFalse(ASMMixinCompiler.getSuper(call("parent", "wide", desc), wide).isDefined()); + assertEquals(3, wide.stack().size()); + assertSame(receiver, wide.peek(2)); + } + } + + @Test + void preservesVirtualReadsAndUsesTheDescriptorAfterTheSuperclassCallback() throws Exception { + List events = new ArrayList<>(); + MethodInsnNode call = call("parent", "target", "()I"); + Type switched = Type.getObjectType("codechicken/multipart/test/superlookup/Switched"); + Option selected = new Some<>(lookup("target", "target", "()I").get()); + ClassInfo parent = new TestInfo() { + + @Override + public Option findPublicImpl(String name, String desc) { + events.add("find:" + name + desc); + return selected; + } + }; + ClassInfo info = new TestInfo() { + + @Override + public Option superClass() { + events.add("super:" + call.desc); + call.desc = "(J)I"; + call.name = "changed"; + return new Some<>(parent); + } + }; + StackAnalyser stack = new StackAnalyser(OWNER, method("target")) { + + private boolean switchedOwner; + + @Override + public Type owner() { + events.add("owner:" + switchedOwner); + return switchedOwner ? switched : OWNER; + } + + @Override + public StackEntry peek(int index) { + events.add("peek:" + index); + switchedOwner = true; + call.desc = "(I)I"; + return new Load(new This(null), null) { + + @Override + public codechicken.multipart.asm.StackAnalyser.LocalEntry e() { + events.add("load"); + return super.e(); + } + }; + } + }; + events.clear(); // Discard constructor reads; characterize only the query. + Map cache = cache(); + Option previous = cache.put(switched.getInternalName(), info); + try { + assertSame(selected, ASMMixinCompiler.getSuper(call, stack)); + assertEquals( + Arrays.asList("owner:false", "peek:0", "load", "owner:true", "super:(I)I", "find:target(J)I"), + events); + cache.put(switched.getInternalName(), new TestInfo() { + + @Override + public Option superClass() { + return new Some<>(null); + } + }); + call.name = "target"; + assertThrows(NullPointerException.class, () -> ASMMixinCompiler.getSuper(call, stack)); + } finally { + cache.remove(switched.getInternalName()); + if (previous.isDefined()) cache.put(switched.getInternalName(), previous.get()); + } + } + + private static Option lookup(String methodName, String targetName, String desc) { + StackAnalyser stack = stack(methodName); + stack.push(receiver()); + return ASMMixinCompiler.getSuper(call("unrelated/Owner", targetName, desc), stack); + } + + private static StackAnalyser stack(String name) { + return new StackAnalyser(OWNER, method(name)); + } + + private static MethodNode method(String name) { + return new MethodNode(ACC_PUBLIC, name, "()I", null, null); + } + + private static MethodInsnNode call(String owner, String name, String desc) { + return new MethodInsnNode(INVOKESPECIAL, owner, name, desc, false); + } + + private static Load receiver() { + return new Load(new This(OWNER), null); + } + + @SuppressWarnings("unchecked") + private static Map cache() throws Exception { + Field field = ASMMixinCompiler$.class.getDeclaredField("infoCache"); + field.setAccessible(true); + return (Map) field.get(ASMMixinCompiler$.MODULE$); + } + + private abstract static class TestInfo extends ClassInfo { + + @Override + public String name() { + throw new AssertionError("Unexpected name read"); + } + + @Override + public Option superClass() { + throw new AssertionError("Unexpected superclass read"); + } + + @Override + public Iterable interfaces() { + throw new AssertionError("Unexpected interface read"); + } + + @Override + public Iterable methods() { + throw new AssertionError("Unexpected method read"); + } + } + + public static class Grandparent { + + public int inherited() { + return 1; + } + + public int target(int argument) { + return argument; + } + } + + public abstract static class Parent extends Grandparent { + + public int target() { + return 2; + } + + protected int protectedTarget() { + return 3; + } + + private int hidden() { + return 4; + } + + public abstract int abstractTarget(); + + public int two(int first, int second) { + return first + second; + } + + public int wide(long argument) { + return (int) argument; + } + + public int wide(double argument) { + return (int) argument; + } + } + + public abstract static class Child extends Parent { + + @Override + public int target() { + return 5; + } + + public int childOnly() { + return 6; + } + } +}