JDK astubx generator files an inner class of a generic class under the outer class

Aperta
#2,159 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
62/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
java
Ambito
tooling

Direzione di ricerca

Start in jdk-annotations/astubx-generator/src/main/java/com/uber/nullaway/jdkannotations/AstubxGenerator.java, in getAstubxData, where the class key is derived from clazz.type() cut at the first <. Run the two reproducing tests innerClassOfGenericClass and innerClassTypeParamBound in AstubxTest.java to see the missing Outer.Inner record and the wrong Outer2 bound. Done when both tests pass, Outer.Inner gets its own record, and Outer2 no longer receives the inner class's bound.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

The JDK astubx generator files what it records for an inner (non-static) class of a generic class under the outer class's name. The inner class's method models are lost when the outer class declares the same method, and a nullable type-parameter bound of the inner class becomes a bound of the outer class.

I added these two cases to jdk-annotations/astubx-generator/src/test/java/com/uber/nullaway/jdkannotations/AstubxTest.java on master (a03b7bb90) and ran ./gradlew :jdk-annotations:astubx-generator:test --tests 'com.uber.nullaway.jdkannotations.AstubxTest':

  @Test
  public void innerClassOfGenericClass() {
    compilationHelper
        .addSourceLines(
            "Outer.java",
            """
            import org.jspecify.annotations.NullMarked;
            import org.jspecify.annotations.Nullable;
            @NullMarked
            public class Outer<T> {
                public @Nullable String get(@Nullable Object key) { return null; }
                public class Inner {
                    public @Nullable String get(Object key) { return null; }
                }
                public static class Nested {
                    public @Nullable String get(Object key) { return null; }
                }
            }
            """)
        .doTest();
    ImmutableMap<String, MethodAnnotationsRecord> expectedMethodRecords =
        ImmutableMap.of(
            "Outer:java.lang.String get(java.lang.Object)",
            MethodAnnotationsRecord.create(
                ImmutableSet.of("Nullable"),
                ImmutableSet.of(),
                ImmutableMap.of(0, ImmutableSet.of("Nullable")),
                ImmutableSetMultimap.of()),
            "Outer.Inner:java.lang.String get(java.lang.Object)",
            MethodAnnotationsRecord.create(
                ImmutableSet.of("Nullable"),
                ImmutableSet.of(),
                ImmutableMap.of(),
                ImmutableSetMultimap.of()),
            "Outer.Nested:java.lang.String get(java.lang.Object)",
            MethodAnnotationsRecord.create(
                ImmutableSet.of("Nullable"),
                ImmutableSet.of(),
                ImmutableMap.of(),
                ImmutableSetMultimap.of()));
    runTest(expectedMethodRecords, ImmutableMap.of(), ImmutableSet.of("Outer"));
  }

  @Test
  public void innerClassTypeParamBound() {
    compilationHelper
        .addSourceLines(
            "Outer2.java",
            """
            import org.jspecify.annotations.NullMarked;
            import org.jspecify.annotations.Nullable;
            @NullMarked
            public class Outer2<T> {
                public class Inner<U extends @Nullable Object> {}
            }
            """)
        .doTest();
    runTest(
        ImmutableMap.of(),
        ImmutableMap.of("Outer2.Inner", ImmutableSet.of(0)),
        ImmutableSet.of("Outer2"));
  }

Both fail. The record for Outer.Inner is missing, while the static Outer.Nested gets its own record:

AstubxTest > innerClassOfGenericClass FAILED
    java.lang.AssertionError: 
    Expected: <{Outer:java.lang.String get(java.lang.Object)=MethodAnnotationsRecord[methodAnnotations=[Nullable], typeParamNullableUpperbounds=[], argumentAnnotations={0=[Nullable]}, nestedAnnotationInfo={}], Outer.Inner:java.lang.String get(java.lang.Object)=MethodAnnotationsRecord[methodAnnotations=[Nullable], typeParamNullableUpperbounds=[], argumentAnnotations={}, nestedAnnotationInfo={}], Outer.Nested:java.lang.String get(java.lang.Object)=MethodAnnotationsRecord[methodAnnotations=[Nullable], typeParamNullableUpperbounds=[], argumentAnnotations={}, nestedAnnotationInfo={}]}>
         but: was <{Outer:java.lang.String get(java.lang.Object)=MethodAnnotationsRecord[methodAnnotations=[Nullable], typeParamNullableUpperbounds=[], argumentAnnotations={0=[Nullable]}, nestedAnnotationInfo={}], Outer.Nested:java.lang.String get(java.lang.Object)=MethodAnnotationsRecord[methodAnnotations=[Nullable], typeParamNullableUpperbounds=[], argumentAnnotations={}, nestedAnnotationInfo={}]}>

Outer2 gets a nullable bound on its type parameter, which only Inner declares:

AstubxTest > innerClassTypeParamBound FAILED
    java.lang.AssertionError: 
    Expected: <{Outer2.Inner=[0]}>
         but: was <{Outer2=[0]}>

The committed nullaway/src/main/resources/jspecify-jdk.astubx has such entries. HashMap, LinkedHashMap, and TreeMap declare no boolean contains(Object) or boolean remove(Object), but their inner key-set, entry-set, and values classes do, such as HashMap.KeySet, LinkedHashMap.LinkedEntrySet, and TreeMap.Values:

strings nullaway/src/main/resources/jspecify-jdk.astubx | grep -E '(HashMap|TreeMap):boolean (contains|remove)\(java\.lang\.Object\)'
4java.util.HashMap:boolean contains(java.lang.Object)
2java.util.HashMap:boolean remove(java.lang.Object)
:java.util.LinkedHashMap:boolean contains(java.lang.Object)
8java.util.LinkedHashMap:boolean remove(java.lang.Object)
4java.util.TreeMap:boolean contains(java.lang.Object)
2java.util.TreeMap:boolean remove(java.lang.Object)

These classes are package-private, so I have not found a JDK member whose NullAway diagnostics change because of it. A public generic outer class in the current file does not get a wrong bound this way either.

Likely cause (from reading the code, not confirmed by a fix): AstubxGenerator.getAstubxData derives the class key from clazz.type() cut at the first <. For an inner class of a generic class that string is Outer<T>.Inner, so the key becomes Outer. A static nested class prints as Outer.Nested, which has no < before the nested name. Method records and bounds are then stored with put, so the record written last for a key wins.

Assisted-by: Claude Code (claude-opus-5-5)

Lingua principale
Java
Stelle
4.1k
Fork
381
Merge medio
1g 2h
PR unite (30g)
106

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di uber/NullAway

Tutte le issue di uber/NullAway

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.