public void init()

in jdk-javac-plugin/src/main/java/com/uber/nullaway/javacplugin/NullnessAnnotationSerializer.java [79:330]


  public void init(JavacTask task, String... args) {
    String outputDir = args[0];
    Trees trees = Trees.instance(task);
    task.addTaskListener(
        new com.sun.source.util.TaskListener() {

          @Override
          public void finished(com.sun.source.util.TaskEvent e) {
            if (e.getKind() == com.sun.source.util.TaskEvent.Kind.ANALYZE) {
              CompilationUnitTree cu = e.getCompilationUnit();
              new TreePathScanner<@Nullable Void, @Nullable Void>() {
                /* keep a stack of class contexts to handle nested classes */
                final Deque<ClassInfo> classStack = new ArrayDeque<>();

                @Nullable ClassInfo currentClass = null;

                @Override
                public @Nullable Void visitClass(ClassTree classTree, @Nullable Void unused) {
                  Name simpleName = classTree.getSimpleName();
                  if (simpleName.contentEquals("")) {
                    return null; // skip anonymous
                  }
                  ClassSymbol classSym = (ClassSymbol) trees.getElement(getCurrentPath());
                  @SuppressWarnings("ASTHelpersSuggestions")
                  String moduleName =
                      Objects.requireNonNull(classSym.packge().getEnclosingElement())
                          .getQualifiedName()
                          .toString();
                  if (moduleName.isEmpty()) { // unnamed module
                    moduleName = "unnamed";
                  }
                  if (classSym.getModifiers().contains(Modifier.PRIVATE)) {
                    return null; // skip private classes
                  }
                  TypeMirror classType = trees.getTypeMirror(getCurrentPath());
                  boolean hasNullMarked = hasAnnotation(classSym, NULLMARKED_NAME);
                  boolean hasNullUnmarked = hasAnnotation(classSym, NULLUNMARKED_NAME);
                  if (currentClass != null) {
                    // save current class context
                    classStack.push(currentClass);
                  }
                  boolean currentClassHasAnnotation = hasNullMarked || hasNullUnmarked;
                  // build new class context
                  List<TypeParamInfo> classTypeParams = new ArrayList<>();
                  for (TypeParameterTree tp : classTree.getTypeParameters()) {
                    classTypeParams.add(typeParamInfo(tp));
                    if (typeParamHasAnnotation(tp)) {
                      currentClassHasAnnotation = true;
                    }
                  }
                  List<MethodInfo> classMethods = new ArrayList<>();
                  currentClass =
                      new ClassInfo(
                          simpleName.toString(),
                          classType.toString(),
                          hasNullMarked,
                          hasNullUnmarked,
                          classTypeParams,
                          classMethods);
                  super.visitClass(classTree, null);
                  currentClassHasAnnotation =
                      currentClassHasAnnotation || !currentClass.methods().isEmpty();
                  // only save classes containing jspecify annotations
                  if (currentClassHasAnnotation) {
                    moduleClasses
                        .computeIfAbsent(moduleName, k -> new ArrayList<>())
                        .add(currentClass);
                  }
                  // restore previous class context
                  currentClass = !classStack.isEmpty() ? classStack.pop() : null;
                  return null;
                }

                @Override
                public @Nullable Void visitMethod(MethodTree methodTree, @Nullable Void unused) {
                  MethodSymbol mSym = (MethodSymbol) trees.getElement(getCurrentPath());
                  if (mSym.getModifiers().contains(Modifier.PRIVATE)) {
                    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;
                      CreateNestedAnnotationInfoVisitor visitor =
                          new CreateNestedAnnotationInfoVisitor();
                      mSym.getReturnType().accept(visitor, null);
                      Set<NestedAnnotationInfo> nested = visitor.getNestedAnnotationInfoSet();
                      if (!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
                  for (int idx = 0; idx < mSym.getParameters().size(); idx++) {
                    Symbol.VarSymbol vSym = mSym.getParameters().get(idx);
                    if (hasJSpecifyAnnotationDeep(vSym.asType())) {
                      methodHasAnnotations = true;
                      CreateNestedAnnotationInfoVisitor visitor =
                          new CreateNestedAnnotationInfoVisitor();
                      vSym.asType().accept(visitor, null);
                      Set<NestedAnnotationInfo> nested = visitor.getNestedAnnotationInfoSet();
                      if (!nested.isEmpty()) {
                        nestedAnnotationsMap.put(idx, nested);
                      }
                    }
                  }
                  List<TypeParamInfo> methodTypeParams = new ArrayList<>();
                  for (TypeParameterTree tp : methodTree.getTypeParameters()) {
                    if (typeParamHasAnnotation(tp)) {
                      methodHasAnnotations = true;
                    }
                    methodTypeParams.add(typeParamInfo(tp));
                  }
                  MethodInfo methodInfo =
                      new MethodInfo(
                          returnType,
                          mSym.toString(),
                          hasNullMarked,
                          hasNullUnmarked,
                          methodTypeParams,
                          nestedAnnotationsMap);
                  // only add this method if it uses JSpecify annotations
                  if (currentClass != null && methodHasAnnotations) {
                    currentClass.methods().add(methodInfo);
                  }
                  return super.visitMethod(methodTree, null);
                }

                private TypeParamInfo typeParamInfo(TypeParameterTree tp) {
                  String name = tp.getName().toString();
                  List<String> bounds = new ArrayList<>();
                  for (var b : tp.getBounds()) {
                    bounds.add(b.toString());
                  }
                  return new TypeParamInfo(name, bounds);
                }

                private boolean hasAnnotation(Symbol sym, String fqn) {
                  return sym.getAnnotationMirrors().stream()
                      .map(AnnotationMirror::getAnnotationType)
                      .map(Object::toString)
                      .anyMatch(fqn::equals);
                }

                private boolean typeParamHasAnnotation(TypeParameterTree tp) {
                  // Get the path to the type parameter relative to the current class path
                  TreePath tpPath = TreePath.getPath(getCurrentPath(), tp);
                  if (tpPath == null) {
                    return false;
                  }
                  // Get the symbol for the type parameter
                  Symbol tpSym = (Symbol) trees.getElement(tpPath);
                  if (tpSym == null) {
                    return false;
                  }
                  TypeVariable tv = (TypeVariable) tpSym.asType();
                  boolean hasAnnotation =
                      hasJSpecifyAnnotationDeep(tv.getUpperBound())
                          || hasJSpecifyAnnotationDeep(tv.getLowerBound());
                  return hasAnnotation || hasJSpecifyAnnotationDeep(tpSym.asType());
                }

                /*
                 * Checks if a list of {@link AnnotationMirror}s contains any JSpecify nullness
                 * annotations ({@code @Nullable} or {@code @NonNull}).
                 *
                 * @param mirrors the list of {@link AnnotationMirror}s to check
                 * @return {@code true} if any JSpecify nullness annotations are present, {@code
                 *     false} otherwise
                 */
                private boolean typeHasJSpecifyAnnotation(
                    List<? extends AnnotationMirror> mirrors) {
                  for (AnnotationMirror am : mirrors) {
                    String fqn = am.getAnnotationType().toString();
                    if (fqn.equals(NULLABLE_NAME) || fqn.equals(NONNULL_NAME)) {
                      return true;
                    }
                  }
                  return false;
                }

                /*
                 * Recursively checks if a {@code TypeMirror} or any of its nested type components
                 * (like array components or bounds) have a JSpecify nullness annotation.
                 *
                 * <p>This method performs a "deep" search, traversing the structure of a given
                 * type. It returns {@code true} as soon as the first JSpecify annotation is found.
                 *
                 * @param type The {@link TypeMirror} to inspect.
                 * @return Returns {@code true} if {@code type} has JSpecify annotations.
                 */
                private boolean hasJSpecifyAnnotationDeep(@Nullable TypeMirror type) {
                  if (type == null) {
                    return false;
                  }
                  if (typeHasJSpecifyAnnotation(type.getAnnotationMirrors())) {
                    return true;
                  }
                  switch (type.getKind()) {
                    case ARRAY -> {
                      return hasJSpecifyAnnotationDeep(
                          ((javax.lang.model.type.ArrayType) type).getComponentType());
                    }
                    case DECLARED -> {
                      for (TypeMirror arg :
                          ((javax.lang.model.type.DeclaredType) type).getTypeArguments()) {
                        if (hasJSpecifyAnnotationDeep(arg)) {
                          return true;
                        }
                      }
                      return false;
                    }
                    case WILDCARD -> {
                      javax.lang.model.type.WildcardType wt =
                          (javax.lang.model.type.WildcardType) type;
                      return hasJSpecifyAnnotationDeep(wt.getExtendsBound())
                          || hasJSpecifyAnnotationDeep(wt.getSuperBound());
                    }
                    case INTERSECTION -> {
                      for (TypeMirror b :
                          ((javax.lang.model.type.IntersectionType) type).getBounds()) {
                        if (hasJSpecifyAnnotationDeep(b)) {
                          return true;
                        }
                      }
                      return false;
                    }
                    default -> {
                      return false;
                    }
                  }
                }
              }.scan(cu, null);
            } else if (e.getKind() == com.sun.source.util.TaskEvent.Kind.COMPILATION) {
              Gson gson = new GsonBuilder().setPrettyPrinting().create();
              String jsonFileName = "classes-" + UUID.randomUUID() + ".json";
              Path p = Paths.get(outputDir, jsonFileName);
              try {
                Files.writeString(p, gson.toJson(moduleClasses));
              } catch (IOException ex) {
                throw new RuntimeException(ex);
              }
            }
          }
        });
  }