Skip to content

LOMBOK CONCEPT: Resolution

Reinier Zwitserloot edited this page Jun 18, 2021 · 4 revisions

What is resolution?

When parsing a java file, let's say you see this snippet of code, in a method:

com.foo.Bar.baz.quux()

what does this line mean?

You probably want this answer:

com.foo is a package, Bar is a class inside that package (which is declared to extend AbstractList and implements Serializable), it has a public static field called baz, which is of type a.b.Quux and which has a method called quux(); it returns a java.lang.String.

And if you can't get that answer, certainly you'd want the answer:

com.foo is a package, Bar is a class in the package, baz is a static field in that, and quux is a method.

But without the context of the classpath, the imports of this source file, and all the other types and methods in the source file where this snippet occurs none of the above is possible. For example, if the code is written by someone not adhering to the java code conventions, this interpretation, while weird, is possible:

com is a package, foo is a class in the com package, Bar is an inner static class of the com.foo class, baz is a static field in inner class com.foo.Bar, and quux is a static method in that. This interpretation implies various very common code standards were broken (mainly: lower-cased class name, accessing a static member via an expression instead of the type), but it's valid java.

So, during the parse run, all that the parser ends up doing is seeing this as:

it's an invocation of a method named quux with no arguments. The receiver of the method is a Select operation with name baz on a Select operation with name Bar on a Select operation with name foo on an Ident node with name com. What's com stand for? Nobody knows. What's Bar? No idea.

Answering the question more usefully (in terms of: Bar is an inner class) is what 'resolution' is all about. Sometimes this means going rather far, such as: Okay, well, what are the methods of the class com.foo.Bar and for each such method what is the return type and what methods does THAT have, and so on. That all falls under the name 'resolution'.

Okay, so what about it?

Basically we can't do it! We can't ask these questions in lombok. The problem is: The answers to these questions are not available until the compiler is much further along the compilation process: It has a list of all types on the source and class path and then for each such type, what it extends/implements, what its members are, etc. That's the key point: Lombok wants to modify this list: Lombok generates new methods. If the list of all methods of all types is already made, lombok is too late!

So, for lombok to do its thing, lombok has to run before that list is made. But if the list isn't made yet, you can't answer these questions.

Here's a small sampling of commonly requested lombok features which are impossible to write without resolution:

  • @AllArgsConstructor should also include all parameters of the superclass constructor, if there is no no-args super constructor available.
  • @Builder should automatically take into account fields of the parent class.
  • Can I have a 'meta annotation', for example an annotation which is itself annotated with @ToString @Slf4j @AllArgsConstructor, and then put this meta annotation on classes to then imply it means I want @ToString @Slf4j @AllArgsConstructor?

For all these features it's chicken-and-egg: If we wait until the compilation process has gone on far enough that we do resolution, it's too late to add an equals and hashCode method to the class.

Working around the chicken and egg problem: Rounds

In javac in particular, the chicken and egg problem is solved with a concept called 'rounds'. But the rounds concept is very expensive especially when lombok is involved: The rounds concept works on the principle that we get far enough to answer resolution questions, so that transformations and code generation can commence. Then, if something was added/generated, start over: Take all that work of turning Select("Bar", Select("foo", Ident("com")) into ClassRef(package = "com.foo", name = "Bar"), along with a list of the members of com.foo.Bar, and toss it in the trash.

The rounds concept works, but it's relatively hard to make it work right in eclipse in particular, and it's very slow. javac also has the stated goal of being fast, so, it will just stop with errors if it thinks there's no chance compilation is ever going to succeed, but lombok does things the annotation processor API isn't quite designed to do, so for certain lombok features, adding rounds means javac just errors without giving lombok a chance to do its thing.

In short: Solving a resolution requirement for a lombok feature by forcing rounds makes that feature incredibly complicated and high-impact (because it slows down compilation and we don't want 'adding lombok to your project' to make everyone go: 'oh no, now my compilation will slow down to a crawl').

Working around the chicken and egg problem: Lombok's own resolver

Lombok also has its own resolver. Because introspecting the entire classpath and parsing all source files far enough along to be able to figure out which types are available and what methods they have is very resource intensive it is extremely limited.

The only question it can answer is this question: "Is this AST node most likely a class reference to 'X'", where X can be something like: "java.lang.String"? Lombok looks at the other type nodes in this file and the import and package statement and nothing else. It almost always answers correctly but it is a known bug which we are never going to address that it is possible to fake it out: If you have "package c; import a.b.*;" in your file, and class "c.Baz" exists, then a class reference to class "Baz" in this file is a reference to "c.Baz", but if you ask lombok's own resolver if this "Baz" reference is a reference to "a.b.Baz", lombok answers 'yes', eventhough it should have answered 'no'. If you don't use star imports, and don't ever name your classes the same as some class in the java.lang package (both of which are pretty much universally agreed on as being bad code style), lombok's resolver won't fail.

With this resolver, we can at least do things like: Is this explicitly written public boolean equals(Object other) method in fact an override of Object's equals method, or not (it would NOT be, if that Object ref is not in fact a reference to java.lang.Object for example). It's also how we identify lombok's own annotations: Is this @Getter in fact lombok's getter or some other annotation which is coincidentally also named Getter?

So, if your resolution requirements go no further than: Is this class reference in this source file a reference to this well-known type? – then you can use lombok's own resolver. It is fast and has virtually no downsides; it's just very limited in what it can answer.

Asking for features which need resolution

If the feature can only be implemented by forcing more rounds we are highly likely to reject a feature request; it'll be hard or impossible to write, and even if we can manage to make it work, it'll probably have a measurable impact on compilation times. The feature request would have to be really amazing (remove LOTS of boilerplate and it has to be a VERY common scenario) for us to consider paying the cost of adding a round.