heckej / proguard

ProGuard, Java optimizer and obfuscator with additional optimisations for Kotlin lambda's
https://www.guardsquare.com/en/products/proguard
GNU General Public License v2.0
2 stars 0 forks source link

Multiple usages of lambda class <init> or INSTANCE field #21

Closed heckej closed 2 years ago

heckej commented 2 years ago

The current implementation of the lambda merger tries to find the code that must be updated to correctly instantiate a lambda in the following way:

  1. read the InnerClasses and EnclosingMethod attribute of the lambda class to find the enclosing class and enclosing method
  2. if the enclosing method is known: update its code
  3. else if the enclosing method refers to a constant in the constant pool at index 0: assume that the enclosing method is the static <clinit> constructor of the enclosing class

The problem that now arises is that some enclosing classes (1 case is known) contain multiple calls to the INSTANCE field or <init> constructor of a lambda class. This occurs for example in the following code from the Jetpack Compose class androidx.compose.animation.core.InfiniteAnimationPolicyKt:

suspend inline fun <R> withInfiniteAnimationFrameMillis(
    crossinline onFrame: (frameTimeMillis: Long) -> R
): R = withInfiniteAnimationFrameNanos { onFrame(it / 1_000_000L) }

The resulting bytecode contains two similar methods which both call the <init> constructor of the lambda androidx.compose.animation.core.InfiniteAnimationPolicyKt$withInfiniteAnimationFrameMillis$2:

public static final <R extends java.lang.Object> java.lang.Object withInfiniteAnimationFrameMillis(kotlin.jvm.functions.Function1<? super java.lang.Long, ? extends R>, kotlin.coroutines.Continuation<? super R>);
private static final <R extends java.lang.Object> java.lang.Object withInfiniteAnimationFrameMillis$$forInline(kotlin.jvm.functions.Function1<? super java.lang.Long, ? extends R>, kotlin.coroutines.Continuation<? super R>);
heckej commented 2 years ago

The class androidx.compose.animation.core.TransitionKt uses the lambda class androidx.compose.animation.core.TransitionKt$animateValue$2 in multiple methods, which all seem to be annotated as inline. These methods however don't have a predictable name, so the only solution here is search in all methods of the enclosing class for usages of the lambda class that have to be replaced by the new lambda group.

heckej commented 2 years ago

Things get somewhat even worse if we have a look at the class androidx.compose.ui.graphics.vector.PropertyValuesHolderColor: in its method AnimateIn, this class calls the <init> constructor the lambda class androidx.compose.ui.graphics.vector.AnimatorKt$animateColor$$inlined$animateValue$1, which is not even an inner class of the PropertyValuesHolderColor class. The only way to find this usage is to look for usages outside the class that is mentioned as the enclosing class of this lambda class.

Note: the PropertyValuesHolderColor class is, just as some other classes, an internal class of the AnimatorKt class, which is in fact the enclosing class of our lambda class. It could thus perhaps suffice to look for usages in:

Unfortunately, the fact that the class PropertyValuesHolderColor is an internal class, does not mean that it is an inner class of `AnimatorKt´ ...

heckej commented 2 years ago

It seems impossible to have the lambda merging transformation output correct results if only the enclosing class is searched for usages. The alternative seems to be a search through all classes in the worst case, or all classes inside the same (root) package.

heckej commented 2 years ago

In practice, all classes of the same package as the lambda are searched.