Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Help understanding a 'type is made accessible using the less-annotated type' warning

20 views
Skip to first unread message

Rich MacDonald

unread,
Sep 13, 2024, 5:58:20 PM9/13/24
to jspecify...@googlegroups.com
Thanks to everyone working on JSpecify. I think you're doing something very valuable and I will do my best to incorporate your work into my system.

I have seen requests for "real-world" cases. Here is one: I have an existing "domain model framework" to handle various "side-effects and capabilities". For example:

1) All property access must hook into an "access control framework", as not all users will have access to all the field values.
2) Changing the value of a field "dirties" the owner and queues the owners to be persisted at the next database commit.

The solution was to delegate all "get/set" methods to a "Property Accessor" framework "behind" the domain objects. (Smalltalk developers will be familiar.)

So here is a simple java object model. A single nullable field:

public enum MyEnum{
 ONE,
 TWO
}

public static class MyDomain{
 @Nullable MyEnum myEnum;
}


I implement the PropertyAccessor in two parts:

1) A single instance on a static variable, reachable by the domain instances.
2) Every call to the domain "get/set" methods instantiates an instance that exists for the duration of the call.

(Aside: The latter instantiation is obviously hell on the garbage-collector if it ran that way in practice. Luckily, the JIT compiler can inline all this to prevent any new instances being created, for the cost of a few stack variables.)

The complete example code is shown below:


package test.jspecify;

import java.util.function.Consumer;
import java.util.function.Supplier;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

/**
 * MyDomain has a field myEnum with a getter and setter. The field is Nullable.
 * Getters and setters need to execute "side-effect" administrative tasks, such as access-control checking, validation, listeners, etc.
 * Framework solution is to delegate to a "PropertyAccessor" layer.
 * Domain class provides getter and setter to the PropertyAccessor. This provides decoupling between the layers.
 *
 * Implementation note, irrelevant to the @Nullability issue.
 * A single TestGetSetNullableEnum instance is implemented at startup via a static method. It handles all instances of MyDomain.
 *
 * Every call to "get" or "set" the field value of a MyDomain instantiates an EnumNullablePropertyAccessor instance.
 * However, final declarations will allow the JIT to inline that without causing heap allocation. (Stack variables are used instead.)
 */
public class ExamplePropertyAccessorNullability {

// Domain classes

public enum MyEnum{
  ONE,
  TWO
}

public static class MyDomain{
  @Nullable MyEnum myEnum;

   @NonNull EnumNullablePropertyAccessor<MyEnum> myEnum() {
      return getSetEnum.on(
         this,
         ()->myEnum,
         value -> this.myEnum = value
      );
   }
   public @Nullable MyEnum getEnum() {
      return myEnum().get();
   }
   public void setEnum(@Nullable MyEnum value) {
      myEnum().set(value);
   }
}

// Property Accessor classes

static final class TestGetSetNullableEnum<PROPERTY extends Enum<PROPERTY>>{
  final String fieldName;
  final Class<PROPERTY> klass;

  public TestGetSetNullableEnum(String fieldName, Class<PROPERTY> klass) {
    this.fieldName = fieldName;
    this.klass = klass;
  }
  public @NonNull EnumNullablePropertyAccessor<PROPERTY> on(Object source, Supplier<@Nullable PROPERTY> getter, Consumer<@Nullable PROPERTY> setter) {
    return new EnumNullablePropertyAccessor<PROPERTY>(this, source, getter, setter);
  }
}

public static final class EnumNullablePropertyAccessor<PROPERTY extends Enum<PROPERTY>> {
  final TestGetSetNullableEnum<PROPERTY> getSetEnum;
  final Object source;
  final Supplier<@Nullable PROPERTY> getter;
  final Consumer<@Nullable PROPERTY> setter;

  EnumNullablePropertyAccessor(TestGetSetNullableEnum<PROPERTY> getSetEnum, Object source, Supplier<@Nullable PROPERTY> getter, Consumer<@Nullable PROPERTY> setter) {
    this.getSetEnum = getSetEnum;
    this.source = source;
    this.getter = getter;
    this.setter = setter;
  }
  @Nullable PROPERTY get() {
    //access control checking, etc
    return getter.get();
  }
  void set(@Nullable PROPERTY value) {
    //access control checking, dirtying the source instance, two-way association updates, validation, listeners, etc
    setter.accept(value);
  }
}

/**
* Following line causes the warning:
* Unsafe null type conversion (type annotations): The value of type 'ExamplePropertyAccessorNullability.@NonNull TestGetSetNullableEnum<ExamplePropertyAccessorNullability.@NonNull MyEnum>'
* is made accessible using the less-annotated type 'ExamplePropertyAccessorNullability.TestGetSetNullableEnum<ExamplePropertyAccessorNullability.MyEnum>'
*/
  static final TestGetSetNullableEnum<MyEnum> getSetEnum = new TestGetSetNullableEnum<>("myEnum", MyEnum.class);

/**
* Alternative following line has no warnings but is hard to understand:
* Why would a "Nullable PropertyAccessor need to declare a @NonNull?
* Who is causing this to be declared as @NonNull? (Not using package-info.java defaults)
*/
  static final TestGetSetNullableEnum<@NonNull MyEnum> getSetEnum2 = new TestGetSetNullableEnum<>("myEnum", MyEnum.class);

  @NullUnmarked
  static class GetSetDeclarations{
    //still getting the warning
    static final TestGetSetNullableEnum<MyEnum> getSetEnum3 = new TestGetSetNullableEnum<>("myEnum", MyEnum.class);
  }
}


The problem I am having is the unwanted warning:

Unsafe null type conversion (type annotations): The value of type 'ExamplePropertyAccessorNullability.@NonNull TestGetSetNullableEnum<ExamplePropertyAccessorNullability.@NonNull MyEnum>'
is made accessible using the less-annotated type 'ExamplePropertyAccessorNullability.TestGetSetNullableEnum<ExamplePropertyAccessorNullability.MyEnum>'




Part of me "gets it": I have defined a class with an unspecified generic, and all the explicit uses of that generic provide the @NonNull/@Nullable. But I don't understand what is causing the warning.

I have tried the desperate/clueless alternatives, including moving the @Nullable annotations to the other classes. No success yet.

I moved all the field declarations to another static class and added @NullUnmarked to the class, i.e. GetSetDeclarations in the above code. This did not work, which surprised me.

My eclipse -> compiler -> errors/warnings -> null analysis settings are all set at the "warning level". I suppose I could live with turning some of them into "info level" but that is a last resort. (And which one corresponds to the 'less-annotated type'?)

As an aside: If anyone is working with the eclipse team, the "Missing '@NonNullByDefault' annotation on package" is unfortunate because it prevents the developer from setting the default to 'NullUnmarked' to remove the warning.

Any help is appreciated.

Rich MacDonald
macdona...@gmail.com

Rich MacDonald

unread,
Sep 13, 2024, 6:23:47 PM9/13/24
to jspecify-discuss
Nothing like crafting a question to steer me towards an answer. As soon as I posted, I started stripping things away to find the essence. I was missing an annotation in the klass argument of the constructor. The following compiles without warnings.

public class ExamplePropertyAccessorNullability { public enum MyEnum{ ONE, TWO } static final class TestGetSetNullableEnum<PROPERTY extends Enum<PROPERTY>>{ final Class<PROPERTY> klass; //Argument must use @NonNull here to remove the warning public TestGetSetNullableEnum(Class<@NonNull PROPERTY> klass) { this.klass = klass; } } //no compiler warnings static final TestGetSetNullableEnum<MyEnum> getSetEnum = new TestGetSetNullableEnum<MyEnum>(MyEnum.class); }

Rich MacDonald

unread,
Sep 14, 2024, 4:02:37 PM9/14/24
to jspecify-discuss
New question: Same code example but now interested in the interaction between  Nullable/NonNull and inhertance hierarchies.

Expanding on my example, say I need both a "Nullable Enum" property and a "NonNull Enum" property: What is the best way to specify the PropertyAccessors? Starting without regard to inheritance, the following has no warnings:
public class NullVsNonNullEnumPropExample { public enum MyEnum { ONE, TWO } public static abstract class MyDomain { @Nullable MyEnum nullableEnum; @NonNull MyEnum nonNullEnum; @NonNull abstract EnumNullablePropertyAccessor<MyEnum> myEnum(); @NonNull abstract EnumNonNullPropertyAccessor<MyEnum> nonNullEnum(); } interface EnumNullablePropertyAccessor<PROPERTY extends Enum<PROPERTY>> { @Nullable PROPERTY get(); void set(@Nullable PROPERTY value); } interface EnumNonNullPropertyAccessor<PROPERTY extends Enum<PROPERTY>> { @NonNull PROPERTY get(); void set(@NonNull PROPERTY value); } }


But now what about an inheritance hierarchy for the PropertyAccessors. This isn't an "inheritance is bad; don't use it" issue. At the very least, the PropertyAccessors are part of serialization code, so some client is going to iterate through a Collection of PropertyAccessors and wants to call get and set. So a parent interface is useful elsewhere.

Note: I am using "Nullable Enum" and "NonNull Enum" to illustrate the question, but I actually have the entire hierarchy of java primitives, Lists, Sets and Maps, Objects and ObjectReferences. I don't plan on doubling the interface count (Nullable and NonNull for each), because in practice all but the primitives are going to be Nullable. But the core is question is how to "inherit both a Nullable and a NonNull from a parent with generics"?

First naive attempt:
interface PropertyAccessor<PROPERTY>{ PROPERTY get(); void set(PROPERTY value); } interface EnumNullablePropertyAccessor<@Nullable PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} interface EnumNonNullPropertyAccessor<@NonNull PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} @NonNull abstract EnumNullablePropertyAccessor<MyEnum> myEnum(); //Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@Nullable PROPERTY extends Enum<@Nullable PROPERTY>' @NonNull abstract EnumNonNullPropertyAccessor<MyEnum> nonNullEnum(); //Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@NonNull PROPERTY extends Enum<@NonNull PROPERTY>'

A shame this didn't work because it seemed like the simplest thing. (Naive developer at work.)

Given that @NonNull is a subtype of @Nullable, try declaring the parent as @NonNull and the child as @Nullable? This made it worse
public static abstract class MyDomain { @NonNull abstract EnumNullablePropertyAccessor1<MyEnum> myEnum1(); // no warnings @NonNull abstract EnumNullablePropertyAccessor2<MyEnum> myEnum2(); // no warnings @NonNull abstract EnumNullablePropertyAccessor3<MyEnum> myEnum3(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@Nullable PROPERTY extends Enum<@Nullable PROPERTY>' @NonNull abstract EnumNonNullPropertyAccessor<MyEnum> nonNullEnum(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@NonNull PROPERTY extends Enum<@NonNull PROPERTY>' } interface PropertyAccessor<@NonNull PROPERTY>{ PROPERTY get(); void set(PROPERTY value); } interface EnumNullablePropertyAccessor1<PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<@Nullable PROPERTY>{} // Null constraint mismatch: The type '@Nullable PROPERTY extends Enum<PROPERTY extends Enum<PROPERTY>>' is not a valid substitute for the type parameter '@NonNull PROPERTY' interface EnumNullablePropertyAccessor2<PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} // Null constraint mismatch: The type 'PROPERTY extends Enum<PROPERTY>' is not a valid substitute for the type parameter '@NonNull PROPERTY' interface EnumNullablePropertyAccessor3<@Nullable PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} // Null constraint mismatch: The type '@Nullable PROPERTY extends Enum<@Nullable PROPERTY>' is not a valid substitute for the type parameter '@NonNull PROPERTY' interface EnumNonNullPropertyAccessor<@NonNull PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} //no warnings static void testDomain(MyDomain domain) { MyEnum myEnumGet1 = domain.myEnum1().get(); // Contradictory null annotations: method was inferred as 'NullVsNonNullEnumPropExample.@NonNull @Nullable MyEnum get()', but only one of '@NonNull' and '@Nullable' can be effective at any location MyEnum myEnumGet2 = domain.myEnum2().get(); // no warnings MyEnum myEnumGet3 = domain.myEnum3().get(); // Contradictory null annotations: method was inferred as 'NullVsNonNullEnumPropExample.@NonNull @Nullable MyEnum get()', but only one of '@NonNull' and '@Nullable' can be effective at any location MyEnum nonNullEnumGet = domain.nonNullEnum().get(); //no warnings }


Try it with the parent as @Nullable. Nope:
public static abstract class MyDomain { @NonNull abstract EnumNullablePropertyAccessor2<MyEnum> myEnum2(); // no warnings @NonNull abstract EnumNullablePropertyAccessor3<MyEnum> myEnum3(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@Nullable PROPERTY extends Enum<@Nullable PROPERTY>' @NonNull abstract EnumNullablePropertyAccessor4<MyEnum> myEnum4(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@Nullable PROPERTY extends Enum<@Nullable PROPERTY>' @NonNull abstract EnumNonNullPropertyAccessor1<MyEnum> nonNullEnum(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@NonNull PROPERTY extends Enum<@NonNull PROPERTY>' } interface PropertyAccessor<@Nullable PROPERTY>{ PROPERTY get(); void set(PROPERTY value); } interface EnumNullablePropertyAccessor1<PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} // Null constraint mismatch: The type 'PROPERTY extends Enum<PROPERTY>' is not a valid substitute for the type parameter '@Nullable PROPERTY' interface EnumNullablePropertyAccessor2<PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<@Nullable PROPERTY>{} // No warnings interface EnumNullablePropertyAccessor3<@Nullable PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} // No warnings interface EnumNullablePropertyAccessor4<@Nullable PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<@Nullable PROPERTY>{} // No warnings interface EnumNonNullPropertyAccessor1<@NonNull PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<PROPERTY>{} // Null constraint mismatch: The type '@NonNull PROPERTY extends Enum<@NonNull PROPERTY>' is not a valid substitute for the type parameter '@Nullable PROPERTY' interface EnumNonNullPropertyAccessor2<@NonNull PROPERTY extends Enum<PROPERTY>> extends PropertyAccessor<@NonNull PROPERTY>{} // Null constraint mismatch: The type '@NonNull PROPERTY extends Enum<@NonNull PROPERTY>' is not a valid substitute for the type parameter '@Nullable PROPERTY' static void testDomain(MyDomain domain) { @Nullable MyEnum myEnumGet2 = domain.myEnum2().get(); // no warnings @NonNull MyEnum myEnumGet2a = domain.myEnum2().get(); // Null type mismatch (type annotations): required 'NullVsNonNullEnumPropExample.@NonNull MyEnum' but this expression has type 'NullVsNonNullEnumPropExample.@Nullable MyEnum' // IS A NULL CHECK ERROR @Nullable MyEnum myEnumGet3 = domain.myEnum3().get(); // no warnings @NonNull MyEnum myEnumGet3a = domain.myEnum3().get(); // Null type mismatch (type annotations): required 'NullVsNonNullEnumPropExample.@NonNull MyEnum' but this expression has type 'NullVsNonNullEnumPropExample.@Nullable MyEnum' // IS A NULL CHECK ERROR @Nullable MyEnum myEnumGet4 = domain.myEnum4().get(); // no warnings @NonNull MyEnum myEnumGet4a = domain.myEnum4().get(); // Null type mismatch (type annotations): required 'NullVsNonNullEnumPropExample.@NonNull MyEnum' but this expression has type 'NullVsNonNullEnumPropExample.@Nullable MyEnum' MyEnum nonNullEnumGet = domain.nonNullEnum().get(); //no warnings }


Ok, so try a Hail Mary (Spoiler Alert: The pass was incomplete)
public static abstract class MyDomain { @NonNull abstract EnumNullablePropertyAccessor1<MyEnum> myEnum1(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@Nullable PROPERTY extends Enum<@Nullable PROPERTY extends Enum<@Nullable PROPERTY>>' @NonNull abstract EnumNullablePropertyAccessor2<MyEnum> myEnum2(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@Nullable PROPERTY extends Enum<@Nullable PROPERTY extends Enum<@Nullable PROPERTY>>' @NonNull abstract EnumNonNullPropertyAccessor<MyEnum> nonNullEnum(); // Null constraint mismatch: The type 'NullVsNonNullEnumPropExample.MyEnum' is not a valid substitute for the type parameter '@NonNull PROPERTY extends Enum<@NonNull PROPERTY>' } interface PropertyAccessor{ @Nullable Object getObject(); //intended for use with clients that don't care about the PROPERTY type void setObject(@Nullable Object value); //intended for use with clients that don't care about the PROPERTY type. May throw an exception } //Should we declare the type as @Nullable? interface PropertyAccessorNullable1<@Nullable PROPERTY> extends PropertyAccessor{ @Nullable PROPERTY get(); void set(@Nullable PROPERTY value); @Override default Object getObject() { return get(); } @Override @SuppressWarnings("unchecked") default void setObject(@Nullable Object value) { //check cast and throw runtime exception if not set((@Nullable PROPERTY) value); } } //Type declared without Nullable interface PropertyAccessorNullable2<PROPERTY> extends PropertyAccessor{ @Nullable PROPERTY get(); void set(@Nullable PROPERTY value); @Override default Object getObject() { return get(); } @Override @SuppressWarnings("unchecked") default void setObject(@Nullable Object value) { //check cast and throw runtime exception if not set((@Nullable PROPERTY) value); } } interface PropertyAccessorNonNull<@NonNull PROPERTY> extends PropertyAccessor{ @NonNull PROPERTY get(); void set(@NonNull PROPERTY value); @Override default Object getObject() { return get(); } @Override @SuppressWarnings("unchecked") default void setObject(@Nullable Object value) { //check cast and throw runtime exception if not //check if null and throw runtime exception if so set((PROPERTY) value); } } interface EnumNullablePropertyAccessor1<@Nullable PROPERTY extends Enum<@Nullable PROPERTY>> extends PropertyAccessorNullable1<@Nullable PROPERTY>{} interface EnumNullablePropertyAccessor2<@Nullable PROPERTY extends Enum<@Nullable PROPERTY>> extends PropertyAccessorNullable2<@Nullable PROPERTY>{} interface EnumNonNullPropertyAccessor<@NonNull PROPERTY extends Enum<PROPERTY>> extends PropertyAccessorNonNull<PROPERTY>{} static void testDomain(MyDomain domain) { @Nullable MyEnum myEnumGet1 = domain.myEnum1().get(); //no warnings @NonNull MyEnum myEnumGet1a = domain.myEnum1().get(); // Null type mismatch (type annotations): required 'NullVsNonNullEnumPropExample.@NonNull MyEnum' but this expression has type 'NullVsNonNullEnumPropExample.@Nullable MyEnum' // Is a detection error as the result is possibly null @Nullable MyEnum myEnumGet2 = domain.myEnum2().get(); //no warnings @NonNull MyEnum myEnumGet2a = domain.myEnum2().get(); // Null type mismatch (type annotations): required 'NullVsNonNullEnumPropExample.@NonNull MyEnum' but this expression has type 'NullVsNonNullEnumPropExample.@Nullable MyEnum' // Is a detection error as the result is possibly null @Nullable MyEnum nonNullEnumGet1 = domain.nonNullEnum().get(); //no warnings @NonNull MyEnum nonNullEnumGet2 = domain.nonNullEnum().get(); //no warnings }


If anyone (a lot more knowledgeable than me) has a solution, please share. But as far as I can tell, Nullable/NonNull is not happy within generic class hierarchy definitions.

I'll keep hammering away. Since the basic interface (WITHOUT inheritance) seems to work ok, the path forward may have to be to abandon the inheritance class hierarchy in the definitions. Meaning that EnumNullablePropertyAccessor and EnumNonNullPropertyAccessor would remain exactly as declared in the initial example of this post. Behind the scenes, implementations can use whatever inheritance is useful. And clients of the class hierarchy can always use other tricks instead of relying on an interface inheritance.

Hope this was interesting to someone else.
Reply all
Reply to author
Forward
0 new messages