Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ad3a0f3
to use NestedAnnotationInfo and Immutable classes
haewiful Jan 8, 2026
4358508
add nested annotation fields to JSON file using NestedAnnotationInfo …
haewiful Jan 8, 2026
d66e0e4
Merge branch 'uber:master' into nested-annotations
haewiful Jan 8, 2026
c2fb5ff
minor changes
haewiful Jan 8, 2026
4c9211b
add type adaptor for ImmutableList in AstubxGenerator
haewiful Jan 9, 2026
582aade
change comments
haewiful Jan 9, 2026
e7e19be
clean up
haewiful Jan 9, 2026
b21c4d2
make field private
haewiful Jan 9, 2026
0d0f0de
for consistency
haewiful Jan 9, 2026
4958718
coderabbit suggestions
haewiful Jan 9, 2026
d83bc26
copy NestedAnnotationInfo into project
haewiful Jan 11, 2026
981ec0c
Merge branch 'master' into nested-annotations
msridhar Jan 14, 2026
d5cc811
suggested simplification in CreateNestedAnnotationInfoVisitor
haewiful Jan 14, 2026
8606f18
code review comments
haewiful Jan 14, 2026
1ae87d2
modify array element type test
haewiful Jan 14, 2026
42b3f41
change return type and add getter method
haewiful Jan 14, 2026
e8b97f0
javadoc
haewiful Jan 14, 2026
997a76b
Merge branch 'master' into nested-annotations
msridhar Jan 15, 2026
000f06c
Update jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/C…
haewiful Jan 15, 2026
e879332
Update jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/C…
haewiful Jan 15, 2026
ad29bce
Update jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/N…
haewiful Jan 15, 2026
06c4233
Update jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/N…
haewiful Jan 15, 2026
e49e75c
new test case
haewiful Jan 15, 2026
c558007
Merge branch 'master' into nested-annotations
msridhar Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.uber.nullaway.jdkannotations;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.reflect.TypeToken;
import com.uber.nullaway.javacplugin.NullnessAnnotationSerializer.ClassInfo;
import com.uber.nullaway.javacplugin.NullnessAnnotationSerializer.MethodInfo;
Expand All @@ -13,6 +16,7 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
Expand Down Expand Up @@ -164,7 +168,25 @@ private static Map<String, List<ClassInfo>> parseJson(String jsonDirPath) {
throw new IllegalStateException("No JSON files found in: " + jsonDirPath);
}

Gson gson = new Gson();
Gson gson =
new GsonBuilder()
.registerTypeAdapter(
ImmutableList.class,
(JsonDeserializer<ImmutableList<?>>)
(json, type, context) -> {
// Get type inside the list
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
Type innerType = typeArgs.length > 0 ? typeArgs[0] : Object.class;

// Get as ArrayList
List<?> standardList =
context.deserialize(
json, TypeToken.getParameterized(List.class, innerType).getType());

// Convert to Guava ImmutableList
return ImmutableList.copyOf(standardList);
})
.create();
Type parsedType = new TypeToken<Map<String, List<ClassInfo>>>() {}.getType();

// parse JSON file
Expand Down
2 changes: 2 additions & 0 deletions jdk-javac-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies {

implementation libs.gson
implementation libs.jspecify
implementation project(':nullaway')
implementation libs.guava

testImplementation libs.junit4
testImplementation(libs.error.prone.test.helpers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.uber.nullaway.javacplugin;

import com.google.common.collect.ImmutableList;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.uber.nullaway.librarymodel.NestedAnnotationInfo;
import com.uber.nullaway.librarymodel.NestedAnnotationInfo.Annotation;
import com.uber.nullaway.librarymodel.NestedAnnotationInfo.TypePathEntry;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class CreateNestedAnnotationInfoVisitor
extends Types.DefaultTypeVisitor<Set<NestedAnnotationInfo>, @Nullable Void> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're storing the nestedAnnotationInfoSet in a field, I don't think we need to return it from every visitor method. Instead, make this:

Suggested change
extends Types.DefaultTypeVisitor<Set<NestedAnnotationInfo>, @Nullable Void> {
extends Types.DefaultTypeVisitor<@Nullable Void, @Nullable Void> {

Then, return null from every visitXXX method, but add a getter method getNestedAnnotationInfoSet() so that the client can retrieve the set at the end.


private ArrayDeque<TypePathEntry> path;
private Set<NestedAnnotationInfo> nestedAnnotationInfoList;

public CreateNestedAnnotationInfoVisitor() {
path = new ArrayDeque<>();
nestedAnnotationInfoList = new HashSet<>();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Make visitor state safer (final fields + clearer naming + avoid exposing mutable internals).

path/nestedAnnotationInfoList are mutable instance state and the returned Set is the same object (callers can mutate it). At minimum: mark fields final, rename nestedAnnotationInfoListnestedAnnotationInfoSet, and consider returning an immutable copy at the end of the top-level call.

Proposed diff
-  private ArrayDeque<TypePathEntry> path;
-  private Set<NestedAnnotationInfo> nestedAnnotationInfoList;
+  private final ArrayDeque<TypePathEntry> path = new ArrayDeque<>();
+  private final Set<NestedAnnotationInfo> nestedAnnotationInfoSet = new HashSet<>();

   public CreateNestedAnnotationInfoVisitor() {
-    path = new ArrayDeque<>();
-    nestedAnnotationInfoList = new HashSet<>();
   }
🤖 Prompt for AI Agents
In
@jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/CreateNestedAnnotationInfoVisitor.java
around lines 23 - 29, Make the visitor's state immutable and avoid exposing
mutable internals: mark the fields path and nestedAnnotationInfoList as final
and rename nestedAnnotationInfoList → nestedAnnotationInfoSet for clarity; keep
path private/final (ArrayDeque<TypePathEntry>) and do not expose it. Wherever
the visitor currently returns its set (e.g., a getNestedAnnotationInfo or the
top-level visit method), return an immutable copy (e.g.,
Set.copyOf(nestedAnnotationInfoSet) or Collections.unmodifiableSet(new
HashSet<>(nestedAnnotationInfoSet))) instead of the internal collection so
callers cannot mutate internal state.


@Override
public @Nullable Set<NestedAnnotationInfo> visitClassType(
Type.ClassType classType, @Nullable Void unused) {
List<Type> typeArguments = classType.getTypeArguments();
if (!typeArguments.isEmpty()) {
for (int idx = 0; idx < typeArguments.size(); idx++) {
path.addLast(new TypePathEntry(TypePathEntry.Kind.TYPE_ARGUMENT, idx));
Type typeArg = typeArguments.get(idx);
ImmutableList<TypePathEntry> typePath = getTypePath();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not store this in a local variable here, since it will do a copy of path for every type variable, whether it is annotated or not. Instead, just call getTypePath() at lines 45 and 47

NestedAnnotationInfo nestedAnnotationInfo = null;
if (hasNullableAnnotation(typeArg)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NULLABLE, typePath);
} else if (hasNonNullAnnotation(typeArg)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NONNULL, typePath);
}
if (nestedAnnotationInfo != null) {
nestedAnnotationInfoList.add(nestedAnnotationInfo);
}
typeArg.accept(this, null);
path.removeLast();
}
}
return nestedAnnotationInfoList;
}

@Override
public @Nullable Set<NestedAnnotationInfo> visitArrayType(
Type.ArrayType t, @Nullable Void unused) {
path.addLast(new TypePathEntry(TypePathEntry.Kind.ARRAY_ELEMENT, -1));
ImmutableList<TypePathEntry> typePath = getTypePath();
NestedAnnotationInfo nestedAnnotationInfo = null;
if (hasNullableAnnotation(t)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NULLABLE, typePath);
} else if (hasNonNullAnnotation(t)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NONNULL, typePath);
}
if (nestedAnnotationInfo != null) {
nestedAnnotationInfoList.add(nestedAnnotationInfo);
}
t.elemtype.accept(this, null);
path.removeLast();
return nestedAnnotationInfoList;
}

@Override
public @Nullable Set<NestedAnnotationInfo> visitWildcardType(
Type.WildcardType t, @Nullable Void unused) {
// Upper Bound (? extends T)
NestedAnnotationInfo nestedAnnotationInfo = null;
if (t.getExtendsBound() != null) {
path.addLast(new TypePathEntry(TypePathEntry.Kind.WILDCARD_BOUND, 0));
ImmutableList<TypePathEntry> typePath = getTypePath();
Type upperBound = t.getExtendsBound();
if (hasNullableAnnotation(upperBound)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NULLABLE, typePath);
} else if (hasNonNullAnnotation(upperBound)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NONNULL, typePath);
}
if (nestedAnnotationInfo != null) {
nestedAnnotationInfoList.add(nestedAnnotationInfo);
}
t.getExtendsBound().accept(this, null);
path.removeLast();
}

// Lower Bound (? super T)
if (t.getSuperBound() != null) {
path.addLast(new TypePathEntry(TypePathEntry.Kind.WILDCARD_BOUND, 1));
ImmutableList<TypePathEntry> typePath = getTypePath();
Type lowerBound = t.getSuperBound();
if (hasNullableAnnotation(lowerBound)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NULLABLE, typePath);
} else if (hasNonNullAnnotation(lowerBound)) {
nestedAnnotationInfo = new NestedAnnotationInfo(Annotation.NONNULL, typePath);
}
if (nestedAnnotationInfo != null) {
nestedAnnotationInfoList.add(nestedAnnotationInfo);
}
t.getSuperBound().accept(this, null);
path.removeLast();
}

return nestedAnnotationInfoList;
}

@Override
public @Nullable Set<NestedAnnotationInfo> visitType(Type t, @Nullable Void unused) {
// just return null (no nested types)
return null;
}

private boolean hasNullableAnnotation(TypeMirror type) {
if (type == null) {
return false;
}
for (AnnotationMirror annotation : type.getAnnotationMirrors()) {
String qualifiedName =
((TypeElement) annotation.getAnnotationType().asElement()).getQualifiedName().toString();
if (qualifiedName.equals("org.jspecify.annotations.Nullable")) {
return true;
}
}
return false;
}

private boolean hasNonNullAnnotation(TypeMirror type) {
if (type == null) {
return false;
}
for (AnnotationMirror annotation : type.getAnnotationMirrors()) {
String qualifiedName =
((TypeElement) annotation.getAnnotationType().asElement()).getQualifiedName().toString();
if (qualifiedName.equals("org.jspecify.annotations.NonNull")) {
return true;
}
}
return false;
}

private ImmutableList<TypePathEntry> getTypePath() {
return ImmutableList.copyOf(path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.uber.nullaway.librarymodel.NestedAnnotationInfo;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -25,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Modifier;
Expand Down Expand Up @@ -54,7 +56,8 @@ public record MethodInfo(
String name,
boolean nullMarked,
boolean nullUnmarked,
List<TypeParamInfo> typeParams) {}
List<TypeParamInfo> typeParams,
Map<Integer, Set<NestedAnnotationInfo>> nestedAnnotationsList) {}

public record ClassInfo(
String name,
Expand Down Expand Up @@ -153,22 +156,31 @@ public void finished(com.sun.source.util.TaskEvent e) {
return super.visitMethod(methodTree, null);
}
boolean methodHasAnnotations = false;
Map<Integer, Set<NestedAnnotationInfo>> nestedAnnotationsMap = new HashMap<>();
String returnType = "";
if (methodTree.getReturnType() != null) {
returnType += mSym.getReturnType().toString();
if (hasJSpecifyAnnotationDeep(mSym.getReturnType())) {
methodHasAnnotations = true;
}
Set<NestedAnnotationInfo> nested =
mSym.getReturnType().accept(new CreateNestedAnnotationInfoVisitor(), null);
if (nested != null && !nested.isEmpty()) {
nestedAnnotationsMap.put(-1, nested);
}
}
boolean hasNullMarked = hasAnnotation(mSym, NULLMARKED_NAME);
boolean hasNullUnmarked = hasAnnotation(mSym, NULLUNMARKED_NAME);
methodHasAnnotations = methodHasAnnotations || hasNullMarked || hasNullUnmarked;
// check each parameter annotations
if (!methodHasAnnotations) {
for (Symbol.VarSymbol vSym : mSym.getParameters()) {
if (hasJSpecifyAnnotationDeep(vSym.asType())) {
methodHasAnnotations = true;
break;
for (int idx = 0; idx < mSym.getParameters().size(); idx++) {
Symbol.VarSymbol vSym = mSym.getParameters().get(idx);
if (hasJSpecifyAnnotationDeep(vSym.asType())) {
methodHasAnnotations = true;
Set<NestedAnnotationInfo> nested =
vSym.asType().accept(new CreateNestedAnnotationInfoVisitor(), null);
if (nested != null && !nested.isEmpty()) {
nestedAnnotationsMap.put(idx, nested);
}
}
}
Expand All @@ -185,7 +197,8 @@ public void finished(com.sun.source.util.TaskEvent e) {
mSym.toString(),
hasNullMarked,
hasNullUnmarked,
methodTypeParams);
methodTypeParams,
nestedAnnotationsMap);
// only add this method if it uses JSpecify annotations
if (currentClass != null && methodHasAnnotations) {
currentClass.methods().add(methodInfo);
Expand Down
Loading