ClassTransform.transformingMethodBodies andThen composes incorrectly

XMLWordPrintable

      A DESCRIPTION OF THE PROBLEM :
      I would expect to be able to use java.lang.classfile.ClassTransform.transformingMethodBodies like this to transform two methods:

      ClassTransform fooBarTransform = ClassTransform.transformingMethodBodies(
              method -> method.methodName().equalsString("foo"),
              fooTransform
      ).andThen(ClassTransform.transformingMethodBodies(
              method -> method.methodName().equalsString("bar"),
              barTransform
      ));

      Bur surprisingly, andThen ends up composing such that the above fooTransform and barTransform is never applied! Looking at the source of jdk.internal.classfile.impl.ClassMethodTransform, it appears there is an optimization in the override of andThen that executes both transforms under the intersection of their filters (which in the above example means it will never trigger since a method cannot be named both foo and bar). This seems like very surprising behavior and different from the declared semantics of andThen. Sabotaging this "optimization" by wrapping one side in ClassTransform.ofStateful reverts the implementation to expected behavior.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      fooTransform is applied for methods named foo
      barTransform is applied for method named bar
      ACTUAL -
      fooTransform and barTransform is applied for methods named foo and bar (i.e. never)

      ---------- BEGIN SOURCE ----------
      import java.io.IOException;
      import java.io.InputStream;
      import java.lang.classfile.*;
      import java.util.Objects;
      import java.util.concurrent.atomic.AtomicBoolean;

      public class Example {

          public static void main(String[] args) {

              byte[] classBytes;
              String classPath = Example.class.getName().replace('.', '/') + ".class";
              
              try(InputStream stream = Objects.requireNonNull(Example.class.getClassLoader().getResourceAsStream(classPath))) {
                  classBytes = stream.readAllBytes();
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }

              var classFile = ClassFile.of();
              var classModel = classFile.parse(classBytes);

              AtomicBoolean triggered = new AtomicBoolean(false);

              classFile.transformClass(classModel,
                      ClassTransform.transformingMethodBodies(
                              method -> method.methodName().equalsString("foo"),
                              CodeTransform.endHandler(_ -> triggered.set(true))
                      )
              );

              if (!triggered.getAndSet(false)) {
                  throw new RuntimeException("Expected triggered first");
              }

              classFile.transformClass(classModel,
                      ClassTransform.transformingMethodBodies(
                              method -> method.methodName().equalsString("foo"),
                              CodeTransform.endHandler(_ -> triggered.set(true))
                      ).andThen(ClassTransform.ofStateful(() -> ClassTransform.transformingMethodBodies(
                              method -> method.methodName().equalsString("bar"),
                              CodeTransform.endHandler(_ -> triggered.set(true))
                      )))
              );

              if (!triggered.getAndSet(false)) {
                  throw new RuntimeException("Expected triggered second");
              }

              classFile.transformClass(classModel,
                      ClassTransform.transformingMethodBodies(
                              method -> method.methodName().equalsString("foo"),
                              CodeTransform.endHandler(_ -> triggered.set(true))
                      ).andThen(ClassTransform.transformingMethodBodies(
                              method -> method.methodName().equalsString("bar"),
                              CodeTransform.endHandler(_ -> triggered.set(true))
                      ))
              );

              if (!triggered.getAndSet(false)) {
                  throw new RuntimeException("Expected triggered third");
              }
          }

          public void foo() { }
          public void bar() { }
      }
      ---------- END SOURCE ----------

            Assignee:
            Chen Liang
            Reporter:
            Webbug Group
            Votes:
            1 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: