JDK astubx generator files an inner class of a generic class under the outer class
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 62/100
Research direction
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.
Written by the indexing model from the issue text.
Description
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)
- Dominant language
- Java
- Stars
- 4.1k
- Forks
- 379
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 106
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from uber/NullAway
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
uber/NullAway#2145 · 1 reaction ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
uber/NullAway#2058 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
uber/NullAway#1899 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 15/100
uber/NullAway#2160 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 50/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
johanhaleby/occurrent#1505 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
FasterXML/jackson-databind#6443 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
finos/fluxnova-bpm-platform#387 ·
Maintainers usually reply within 4 days
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100