Search code examples
javaaoplombokaspectmethod-signature

Lombok extension methods: Prevalence/priority?


First off: I absolutely LOVE Project Lombok. Awesome tool! There's so many excellent aspects to this 'compile time' library.

Loving the @ExtensionMethods, I have already hit this 'feature' a few times, so now it's time for me to ask this question:

Suppose I have the following classes:

@UtilityClass
public class AObject {
    static public String message(final Object pObject) {
        return "AObject = " + (pObject != null);
    }
}
@UtilityClass
public class AString {
    static public String message(final String pObject) {
        return "AString = " + (pObject != null);
    }
}
@ExtensionMethod({ AObject.class, AString.class })
public class Run_Object_String {
    public static void main(final String[] args) {
        System.out.println("\nRun_Object_String.main()");
        final String s = "Bier!";
        final Object o = new Object();

        System.out.println("Testing s: " + s.message());
        System.out.println("Testing o: " + o.message());
        System.out.println("Testing s: " + s.message());
    }
}
@ExtensionMethod({ AString.class, AObject.class })
public class Run_String_Object {
    public static void main(final String[] args) {
        System.out.println("\nRun_String_Object.main()");
        final String s = "Bier!";
        final Object o = new Object();

        System.out.println("Testing s: " + s.message());
        System.out.println("Testing o: " + o.message());
        System.out.println("Testing s: " + s.message());
    }
}
public class ClassPrevalenceTest {
    public static void main(final String[] args) {
        Run_Object_String.main(args);
        Run_String_Object.main(args);
    }
}

With the output:

Run_Object_String.main()
Testing s: AObject = true
Testing o: AObject = true
Testing s: AObject = true

Run_String_Object.main()
Testing s: AString = true
Testing o: AObject = true
Testing s: AString = true
  • Why is this?
  • Why is the message(String) not called in the first example, even though it has a better method signature fit than message(Object)?
  • Why is @ExtensionMethod dependent on sequence of the arguments?

Here's what I blindly assume:

  • when parsing for ExtensionMethods, Lombok will process annotation values from left to right
    • For Run_Object_String that means: first AObject, then AString
    • For Run_String_Object that means: first AString, then AObject
  • Object-String: When patching AObject into class Run_Object_String, the message(Object) method will be added. And when patching in AString with the message(String) method, it will not be added. Presumably because the message(Object) also matches a call to message(String), so message(String) will not be added.
  • String-Object: When patching AString into class Run_String_Object, the message(String) method will be added. When patching in AObject class with message(Object), the old and present message(String) method will NOT accept the call message(Object), thus the method message(Object) will be added.

So, apart from taking great care of what order I add the @UtilityClass references, are there any other solutions to this?

  • Can the Lombok preprocessor be extended and made more sensible when adding in extension methods?
  • Do you guys have any suggestions regarding this, or an explanation of what is really happening (as opposed to my assumptions)

Solution

  • This is a fascinating use of Lombok I wasn't aware of. The best place I think you could delve to find your answers is the source itself since the docs on this experimental work seems pretty light, understandably.

    Take a look on git here: HandleExtensionMethod.

    I am guessing based on the logic that the area that's effectively "fitting" the right method from the annotation is as below..

    Instead of trying for a "best" fit, it seems to be aiming for a "first" fit.

    That is, it appears to iterate over List<Extension> extensions. Since it's a Java list, we assume ordering is preserved in the order the extensions were specified in the original annotation.

    It appears to simply work in order of the list and return as soon as something matches the right method and type shape.

    Types types = Types.instance(annotationNode.getContext());
            for (Extension extension : extensions) {
                TypeSymbol extensionProvider = extension.extensionProvider;
                if (surroundingTypeSymbol == extensionProvider) continue;
                for (MethodSymbol extensionMethod : extension.extensionMethods) {
                    if (!methodName.equals(extensionMethod.name.toString())) continue;
                    Type extensionMethodType = extensionMethod.type;
                    if (!MethodType.class.isInstance(extensionMethodType) && !ForAll.class.isInstance(extensionMethodType)) continue;
                    Type firstArgType = types.erasure(extensionMethodType.asMethodType().argtypes.get(0));
                    if (!types.isAssignable(receiverType, firstArgType)) continue;
                    methodCall.args = methodCall.args.prepend(receiver);
                    methodCall.meth = chainDotsString(annotationNode, extensionProvider.toString() + "." + methodName);
                    recursiveSetGeneratedBy(methodCall.meth, methodCallNode);
                    return;
                }
            }
    

    You can look at the rest of the code for other insight as there doesn't seem to be too much there (i.e. number of lines) to look at, though admittedly it's an impressive enough a feat to do in that space.