Calling enum's @NullMarked override of @Nullable method not recognized as @NonNull
In a @NullMarked package and the following type hierarchy:
public interface Parent {
default @Nullable String getName() {
return null;
}
}
public enum Child implements Parent {
ONE {
@Override
public String getName() {
return "one";
}
}
}
the following call:
String name = Child.ONE.getName();
System.out.println(name.length());
yields
[NullAway] dereferenced expression name is @Nullable
System.out.println(name.length());
^
(see http://t.uber.com/nullaway )
errorProne version: 2.42.0 NullAway version: 0.12.12 Java version: 24
@chemicL how often does this one come up in your codebase? It's a valid issue, but an annoying one to fix. Under the hood, javac generates a synthetic subclass of Child for the Child.ONE constant to enable dispatch to its getName() method, but this is not exposed at the level where NullAway runs. To NullAway, it looks like getName() is being invoked on an expression of type Child which inherits getName() from Parent. I think fixing this issue in general would be hard, and handling special cases would still require a bunch of hacky code and I'm not sure it's worth it.
I'll note that both IntelliJ and Checker Framework also report a warning for this case. I'm going to mark it as low priority for now.
This is really a Java limitation - you can see an analogous error with plain Java types:
public interface Parent {
default Object getName() { return new Object(); }
}
public enum Child implements Parent {
ONE {
@Override
public String getName() { return "one"; }
}
}
String name = Child.ONE.getName(); // error: incompatible types: Object cannot be converted to String
The issue here is the type of Child.ONE - per the Java Language Spec, the type of this field is just Child, not Child$1.
https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html#jls-8.9.3 ("For each enum constant ... field of type E ...")
Thanks, @jeffrey-easyesi, super useful!
Interesting, thanks for sharing @jeffrey-easyesi!
@msridhar we have just two such occurrences for the same enum, which we changed from
final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListenerDocumentation.ANONYMOUS.getName());
to
String anonymousName = MicrometerObservationListenerDocumentation.ANONYMOUS.getName();
assert anonymousName != null : "anonymousName can not be null"; // NullAway issue?
final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, anonymousName);
I am completely ok with the low priority of this. Especially since perhaps this is something that the JLS could improve upon. It's just something that might be more troubling in some codebases than in others. Perhaps it's worth reporting upstream in case it's something that can be addressed, but I don't have enough background to be able to judge nor report.
Thanks!
@chemicL my guess is the JLS design decision is deliberate and that they are unlikely to revise it. If someone has time maybe they can search around for the discussion on why things were done this way.
It wouldn't make sense to expose an anonymous class as the type of a public field, since anonymous classes aren't expected to be a stable API. Adding or removing a {...} on any constant would become a breaking change.
In most cases this problem can be avoided by declaring methods on the enum itself. If you only need one enum constant, the anonymous class is unnecessary, since you could just write
public enum Child implements Parent {
ONE;
@Override
public String getName() { return "one"; }
}
Or alternatively, if you do need more constants with different getName() implementations, but all returning the same type, you could use an abstract method:
public enum Child implements Parent {
ONE { @Override public String getName() { return "one"; } },
TWO { @Override public String getName() { return "two"; } };
@Override
public abstract String getName();
}