Guardsquare/proguard

Proguard removes method invocation, changing behaviour

xxDark opened this issue · 1 comments

Greetings.
Before running Proguard, I apply some logic to the jar file to rewrite invokedynamic instructions to their equivalent parts.
The result of the transformation is, essentially:

Object object = method_handle;
if (object == null) {
    object = method_handle = ((MethodHandle) java.lang.invoke.CallSite::makeSite).invoke(LambdaMetafactory::metafactory, "accept", MethodType.fromMethodDescriptorString("...", null), new Object[]{MethodType.fromMethodDescriptorString("....", null), MyClass::method_to_call, MethodType.fromMethodDescriptorString("....", null)}, MethodOwner.class)
    .dynamicInvoker().asType(MethodType.fromMethodDescriptorString("(Ljava/lang/Object;)Ljava/lang/Object;", null));
}

Note that MethodType.fromMethodDescriptorString("....", null) is decompiler sugar and MT is pushed on the stack with LDC instructions instead. One of MTs references class with method in question according to LambdaMetafactory::metafactory bootstrap logic.
Despite not having -assumenosideeffects, the invocation of the method is removed. Bootstrap logic succeeds and lambda is created.
I assume that proguard has some builtin logic to detect lambdas and thus not remove them.
Am I correct that the only way to prevent Proguard from removing a method is to write so in the configuration file using keepclassmembers?
Thanks.

Simple example:

public interface MyConsumer<T> {
    public void accept(T t);
}

public class Test {
	public static void main(String[] args) {
		MyConsumer<String> c = consumer();
		c.accept("Hello, World");
	}
	
	private static MyConsumer<String> consumer() {
		return Test::println;
	}
	
	private static void println(String message) {
		System.out.println(message);
	}
}

After some testing, it is indeed seems like Proguard assumes that invocation of MyConsumer::accept has no side effects if there are no implementations of it in the jar file. If I add at least 1 file that references MyConsumer and use it in the code issue disappears.

Small sample, change .zip to .jar
For the sake of simplicity, use Java 8. Jar doesn't include logic necessary to run on newer versions.
Proguard settings:
Shrinking enabled, Test::main is kept
Obfuscation disabled
Optimizations enabled, 5 passes, all boxes under 'Remove' and 'Remove debugging' are unchecked
dontwarn is set to 'java.lang.invoke.**'