All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.java.model.JSymbolMetadataNullabilityHelper Maven / Gradle / Ivy

/*
 * SonarQube Java
 * Copyright (C) 2012-2023 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.java.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationInstance;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationValue;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityData;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityLevel;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityTarget;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonarsource.analyzer.commons.collections.SetUtils;

import static org.sonar.java.model.JSymbolMetadata.noNullabilityAnnotationAt;
import static org.sonar.java.model.JSymbolMetadata.unknownNullabilityAt;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityLevel.CLASS;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityLevel.PACKAGE;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityLevel.VARIABLE;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityTarget.FIELD;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityTarget.LOCAL_VARIABLE;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityTarget.METHOD;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityTarget.PARAMETER;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.NON_NULL;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.NO_ANNOTATION;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.STRONG_NULLABLE;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.UNKNOWN;
import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.WEAK_NULLABLE;

public class JSymbolMetadataNullabilityHelper {

  private JSymbolMetadataNullabilityHelper() {
    // Utility class
  }

  /**
   * List of "strong" Nullable annotations, when something must be checked for nullness.
   */
  private static final Set STRONG_NULLABLE_ANNOTATIONS = SetUtils.immutableSetOf(
    "javax.annotation.CheckForNull",
    "jakarta.annotation.CheckForNull",
    "edu.umd.cs.findbugs.annotations.CheckForNull",
    "org.netbeans.api.annotations.common.CheckForNull",
    // Despite the name, some Nullable annotations are meant to be used as CheckForNull
    // as they are using meta-annotation from javax: @Nonnull(When.MAYBE), same as javax @CheckForNull.
    "org.springframework.lang.Nullable",
    "reactor.util.annotation.Nullable",
    // From the documentation (https://wiki.eclipse.org/JDT_Core/Null_Analysis):
    // For any variable whose type is annotated with @Nullable [...] It is illegal to dereference such a variable for either field or method access.
    "org.eclipse.jdt.annotation.Nullable",
    "org.eclipse.jgit.annotations.Nullable");

  /**
   * List of "weak" annotations, when something can be null, but it may be fine to not check it.
   */
  private static final Set WEAK_NULLABLE_ANNOTATIONS = SetUtils.immutableSetOf(
    "android.annotation.Nullable",
    "android.support.annotation.Nullable",
    "androidx.annotation.Nullable",
    "com.sun.istack.internal.Nullable",
    "com.mongodb.lang.Nullable",
    "edu.umd.cs.findbugs.annotations.Nullable",
    "io.reactivex.annotations.Nullable",
    "io.reactivex.rxjava3.annotations.Nullable",
    "javax.annotation.Nullable",
    "jakarta.annotation.Nullable",
    "org.checkerframework.checker.nullness.compatqual.NullableDecl",
    "org.checkerframework.checker.nullness.compatqual.NullableType",
    "org.checkerframework.checker.nullness.qual.Nullable",
    "org.jetbrains.annotations.Nullable",
    "org.jmlspecs.annotation.Nullable",
    "org.netbeans.api.annotations.common.NullAllowed",
    "org.netbeans.api.annotations.common.NullUnknown");

  /**
   * Nullable annotations is the combination of weak and strong, when something can be null at one point.
   */
  private static final Set NULLABLE_ANNOTATIONS = Collections.unmodifiableSet(
    Stream.of(STRONG_NULLABLE_ANNOTATIONS, WEAK_NULLABLE_ANNOTATIONS)
      .flatMap(Set::stream)
      .collect(Collectors.toSet()));

  /**
   * List of non-null annotation, when something should never be null.
   */
  private static final Set NONNULL_ANNOTATIONS = SetUtils.immutableSetOf(
    "android.annotation.NonNull",
    "android.support.annotation.NonNull",
    "androidx.annotation.NonNull",
    "com.sun.istack.internal.NotNull",
    "com.mongodb.lang.NonNull",
    "edu.umd.cs.findbugs.annotations.NonNull",
    "io.reactivex.annotations.NonNull",
    "io.reactivex.rxjava3.annotations.NonNull",
    "javax.validation.constraints.NotNull",
    "jakarta.validation.constraints.NotNull",
    "lombok.NonNull",
    "org.checkerframework.checker.nullness.compatqual.NonNullDecl",
    "org.checkerframework.checker.nullness.compatqual.NonNullType",
    "org.checkerframework.checker.nullness.qual.NonNull",
    "org.eclipse.jdt.annotation.NonNull",
    "org.eclipse.jgit.annotations.NonNull",
    "org.jetbrains.annotations.NotNull",
    "org.jmlspecs.annotation.NonNull",
    "org.netbeans.api.annotations.common.NonNull",
    "org.springframework.lang.NonNull",
    "reactor.util.annotation.NonNull");

  /**
   * Can have different type depending on the argument "when" value:
   * - ALWAYS or no argument = NONNULL
   * - NEVER or MAYBE = STRONG_NULLABLE
   * - UNKNOWN = WEAK_NULLABLE
   */
  private static final String JAVAX_ANNOTATION_NONNULL = "javax.annotation.Nonnull";

  private static final String JAKARTA_ANNOTATION_NONNULL = "jakarta.annotation.Nonnull";

  /**
   * Target parameters and return values.
   * Only applicable to package.
   */
  private static final String COM_MONGO_DB_LANG_NON_NULL_API = "com.mongodb.lang.NonNullApi";

  /**
   * Target parameters and return values.
   * Only applicable to package.
   */
  private static final String ORG_SPRINGFRAMEWORK_LANG_NON_NULL_API = "org.springframework.lang.NonNullApi";

  /**
   * Target parameters only.
   */
  private static final String JAVAX_ANNOTATION_PARAMETERS_ARE_NONNULL_BY_DEFAULT = "javax.annotation.ParametersAreNonnullByDefault";
  private static final String JAKARTA_ANNOTATION_PARAMETERS_ARE_NONNULL_BY_DEFAULT = "jakarta.annotation.ParametersAreNonnullByDefault";

  /**
   * Target parameters only.
   */
  private static final String JAVAX_ANNOTATION_PARAMETERS_ARE_NULLABLE_BY_DEFAULT = "javax.annotation.ParametersAreNullableByDefault";
  private static final String JAKARTA_ANNOTATION_PARAMETERS_ARE_NULLABLE_BY_DEFAULT = "jakarta.annotation.ParametersAreNullableByDefault";

  /**
   * Target fields only.
   * Only at package level.
   */
  private static final String ORG_SPRINGFRAMEWORK_LANG_NON_NULL_FIELDS = "org.springframework.lang.NonNullFields";

  /**
   * Can have parameters, setting what should be considered as NonNull.
   * PARAMETER, RETURN_TYPE, FIELD
   */
  private static final String ORG_ECLIPSE_JDT_ANNOTATION_NON_NULL_BY_DEFAULT = "org.eclipse.jdt.annotation.NonNullByDefault";

  private static final Set KNOWN_ANNOTATIONS = Stream.of(NULLABLE_ANNOTATIONS, NONNULL_ANNOTATIONS)
      .flatMap(Set::stream)
      .collect(Collectors.toSet());

  private static final Map configuration = new HashMap<>();

  static {
    // Low level annotation (directly annotated)
    configureAnnotation(JSymbolMetadataNullabilityHelper::getIfStrongNullable,
      Arrays.asList(PARAMETER, FIELD, LOCAL_VARIABLE), Collections.singletonList(VARIABLE));
    configureAnnotation(JSymbolMetadataNullabilityHelper::getIfStrongNullable,
      Collections.singletonList(METHOD), Collections.singletonList(NullabilityLevel.METHOD));

    configureAnnotation(JSymbolMetadataNullabilityHelper::getIfNullable,
      Arrays.asList(PARAMETER, FIELD, LOCAL_VARIABLE), Collections.singletonList(VARIABLE));
    configureAnnotation(JSymbolMetadataNullabilityHelper::getIfNullable,
      Collections.singletonList(METHOD), Collections.singletonList(NullabilityLevel.METHOD));

    configureAnnotation(JSymbolMetadataNullabilityHelper::getIfNonNull,
      Arrays.asList(PARAMETER, FIELD, LOCAL_VARIABLE), Collections.singletonList(VARIABLE));
    configureAnnotation(JSymbolMetadataNullabilityHelper::getIfNonNull,
      Collections.singletonList(METHOD), Collections.singletonList(NullabilityLevel.METHOD));

    // Low level: javax.NonNull specific case
    configureAnnotation(JSymbolMetadataNullabilityHelper::getTypeFromNonNull,
      Arrays.asList(PARAMETER, FIELD, LOCAL_VARIABLE), Collections.singletonList(VARIABLE));
    configureAnnotation(JSymbolMetadataNullabilityHelper::getTypeFromNonNull,
      Collections.singletonList(METHOD), Collections.singletonList(NullabilityLevel.METHOD));

    // High level annotation
    configureAnnotation(COM_MONGO_DB_LANG_NON_NULL_API, NON_NULL,
      Arrays.asList(METHOD, PARAMETER), Collections.singletonList(PACKAGE));
    configureAnnotation(ORG_SPRINGFRAMEWORK_LANG_NON_NULL_API, NON_NULL,
      Arrays.asList(METHOD, PARAMETER), Collections.singletonList(PACKAGE));

    configureAnnotation(JAVAX_ANNOTATION_PARAMETERS_ARE_NONNULL_BY_DEFAULT, NON_NULL,
      Collections.singletonList(PARAMETER), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE));
    configureAnnotation(JAVAX_ANNOTATION_PARAMETERS_ARE_NULLABLE_BY_DEFAULT, WEAK_NULLABLE,
      Collections.singletonList(PARAMETER), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE));

    configureAnnotation(JAKARTA_ANNOTATION_PARAMETERS_ARE_NONNULL_BY_DEFAULT, NON_NULL,
      Collections.singletonList(PARAMETER), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE));
    configureAnnotation(JAKARTA_ANNOTATION_PARAMETERS_ARE_NULLABLE_BY_DEFAULT, WEAK_NULLABLE,
      Collections.singletonList(PARAMETER), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE));

    configureAnnotation(ORG_SPRINGFRAMEWORK_LANG_NON_NULL_FIELDS, NON_NULL,
      Collections.singletonList(FIELD), Collections.singletonList(PACKAGE));

    // ORG_ECLIPSE_JDT_ANNOTATION_NON_NULL_BY_DEFAULT specific case (targeting both high and low level)
    configureAnnotation(annotationInstance -> getIfEclipseNonNullByDefault(annotationInstance, "PARAMETER"),
      Collections.singletonList(PARAMETER), Arrays.asList(VARIABLE, NullabilityLevel.METHOD, CLASS, PACKAGE));
    configureAnnotation(annotationInstance -> getIfEclipseNonNullByDefault(annotationInstance, "FIELD"),
      Collections.singletonList(FIELD), Arrays.asList(VARIABLE, NullabilityLevel.METHOD, CLASS, PACKAGE));
    configureAnnotation(annotationInstance -> getIfEclipseNonNullByDefault(annotationInstance, "RETURN_TYPE"),
      Collections.singletonList(METHOD), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE));

    // Add all annotations to the set of known annotations
    KNOWN_ANNOTATIONS.add(JAVAX_ANNOTATION_NONNULL);
    KNOWN_ANNOTATIONS.add(JAKARTA_ANNOTATION_NONNULL);
    KNOWN_ANNOTATIONS.add(COM_MONGO_DB_LANG_NON_NULL_API);
    KNOWN_ANNOTATIONS.add(ORG_SPRINGFRAMEWORK_LANG_NON_NULL_API);
    KNOWN_ANNOTATIONS.add(JAVAX_ANNOTATION_PARAMETERS_ARE_NONNULL_BY_DEFAULT);
    KNOWN_ANNOTATIONS.add(JAKARTA_ANNOTATION_PARAMETERS_ARE_NONNULL_BY_DEFAULT);
    KNOWN_ANNOTATIONS.add(JAVAX_ANNOTATION_PARAMETERS_ARE_NULLABLE_BY_DEFAULT);
    KNOWN_ANNOTATIONS.add(JAKARTA_ANNOTATION_PARAMETERS_ARE_NULLABLE_BY_DEFAULT);
    KNOWN_ANNOTATIONS.add(ORG_SPRINGFRAMEWORK_LANG_NON_NULL_FIELDS);
    KNOWN_ANNOTATIONS.add(ORG_ECLIPSE_JDT_ANNOTATION_NON_NULL_BY_DEFAULT);
  }

  private static void configureAnnotation(String name, NullabilityType type, List targets, List levels) {
    configureAnnotation(annotation -> annotationType(annotation).fullyQualifiedName().equals(name) ? type : NO_ANNOTATION, targets, levels);
  }

  private static void configureAnnotation(Function typeFromAnnotation, List targets, List levels) {
    for (NullabilityTarget target : targets) {
      for (NullabilityLevel level : levels) {
        ConfigurationKey key = new ConfigurationKey(target, level);
        configuration.computeIfAbsent(key, k -> new TypesForAnnotations()).add(typeFromAnnotation);
      }
    }
  }

  /**
   * Return the Nullability data given the metadata of the current symbol, a level and a target.
   */
  public static NullabilityData getNullabilityDataAtLevel(SymbolMetadata metadata, NullabilityTarget target, NullabilityLevel level) {
    TypesForAnnotations typeForAnnotations = configuration.get(new ConfigurationKey(target, level));
    if (typeForAnnotations != null) {
      return getNullabilityDataAtLevel(new HashSet<>(), metadata, level, false, typeForAnnotations);
    }
    return noNullabilityAnnotationAt(level);
  }

  private static NullabilityData getNullabilityDataAtLevel(Set knownTypes, SymbolMetadata metadata,
    NullabilityLevel level, boolean isMetaAnnotated, TypesForAnnotations typeForAnnotations) {
    // Check if the symbol is directly annotated
    NullabilityData directlyAnnotated = getNullabilityData(metadata, level, isMetaAnnotated, typeForAnnotations);
    if (directlyAnnotated.type() != NO_ANNOTATION) {
      return directlyAnnotated;
    }
    for (AnnotationInstance annotationInstance : metadata.annotations()) {
      Symbol annotationSymbol = annotationInstance.symbol();
      Type annotationType = annotationSymbol.type();
      if (knownTypes.add(annotationType) && !KNOWN_ANNOTATIONS.contains(annotationType(annotationInstance).fullyQualifiedName())) {
        // Only do recursion when we face unknown annotations, as we already know the nullability impact and might contain contradicting
        // annotations.
        NullabilityData nullabilityData = getNullabilityDataAtLevel(knownTypes, annotationSymbol.metadata(), level,
          true, typeForAnnotations);
        if (nullabilityData.type() != NO_ANNOTATION) {
          return nullabilityData;
        }
      }
    }
    return noNullabilityAnnotationAt(level);
  }

  private static NullabilityData getNullabilityData(SymbolMetadata metadata,
    NullabilityLevel level,
    boolean isMetaAnnotated,
    TypesForAnnotations typeForAnnotations) {
    NullabilityType nullabilityType = NullabilityType.NO_ANNOTATION;
    AnnotationInstance annotationInstance = null;
    for (AnnotationInstance annotation : metadata.annotations()) {
      NullabilityType typeFromAnnotation = typeForAnnotations.getTypeFromAnnotation(annotation);
      if (typeFromAnnotation.ordinal() > nullabilityType.ordinal()) {
        nullabilityType = typeFromAnnotation;
        annotationInstance = annotation;
      }
    }
    if (nullabilityType == UNKNOWN) {
      return unknownNullabilityAt(level);
    } else if (annotationInstance == null) {
      return noNullabilityAnnotationAt(level);
    }
    return new JSymbolMetadata.JNullabilityData(nullabilityType, level,
      annotationInstance, metadata.findAnnotationTree(annotationInstance), isMetaAnnotated);
  }

  private static NullabilityType getIfStrongNullable(AnnotationInstance annotation) {
    if (isStrongNullableAnnotation(annotationType(annotation))) {
      return STRONG_NULLABLE;
    }
    return NO_ANNOTATION;
  }

  private static boolean isStrongNullableAnnotation(Type type) {
    return STRONG_NULLABLE_ANNOTATIONS.contains(type.fullyQualifiedName());
  }

  private static NullabilityType getIfNullable(AnnotationInstance annotation) {
    if (isNullableAnnotation(annotationType(annotation))) {
      return WEAK_NULLABLE;
    }
    return NO_ANNOTATION;
  }

  private static boolean isNullableAnnotation(Type type) {
    return NULLABLE_ANNOTATIONS.contains(type.fullyQualifiedName());
  }

  private static NullabilityType getIfNonNull(AnnotationInstance annotation) {
    if (isNonNullAnnotation(annotationType(annotation))) {
      return annotation.values().isEmpty() ? NON_NULL : UNKNOWN;
    }
    return NO_ANNOTATION;
  }

  private static boolean isNonNullAnnotation(Type type) {
    return NONNULL_ANNOTATIONS.contains(type.fullyQualifiedName());
  }

  private static NullabilityType getTypeFromNonNull(AnnotationInstance annotation) {
    if (JAVAX_ANNOTATION_NONNULL.equals(annotationType(annotation).fullyQualifiedName())
    || JAKARTA_ANNOTATION_NONNULL.equals(annotationType(annotation).fullyQualifiedName())) {
      List values = annotation.values();
      if (values.isEmpty() || checkAnnotationParameter(values, "when", "ALWAYS")) {
        return NON_NULL;
      } else if (checkAnnotationParameter(values, "when", "UNKNOWN")) {
        return WEAK_NULLABLE;
      } else {
        // when=NEVER or when=MAYBE
        return STRONG_NULLABLE;
      }
    }
    return NO_ANNOTATION;
  }

  private static NullabilityType getIfEclipseNonNullByDefault(AnnotationInstance annotation, String expectedValue) {
    if (ORG_ECLIPSE_JDT_ANNOTATION_NON_NULL_BY_DEFAULT.equals(annotationType(annotation).fullyQualifiedName())) {
      return (annotation.values().isEmpty() || checkAnnotationParameter(annotation.values(), "value", expectedValue)) ? NON_NULL : NO_ANNOTATION;
    }
    return NO_ANNOTATION;
  }

  private static Type annotationType(AnnotationInstance annotation) {
    return annotation.symbol().type();
  }

  private static boolean checkAnnotationParameter(List valuesForAnnotation, String fieldName, String expectedValue) {
    return valuesForAnnotation.stream()
      .filter(annotationValue -> fieldName.equals(annotationValue.name()))
      .anyMatch(annotationValue -> isExpectedValue(annotationValue.value(), expectedValue));
  }

  private static boolean isExpectedValue(Object annotationValue, String expectedValue) {
    if (annotationValue instanceof Object[]) {
      return containsValue((Object[]) annotationValue, expectedValue);
    }
    return annotationValue instanceof Symbol && expectedValue.equals(((Symbol) annotationValue).name());
  }

  private static boolean containsValue(Object[] annotationValue, String expectedValue) {
    return Arrays.stream(annotationValue).map(Symbol.class::cast).anyMatch(symbol -> expectedValue.equals(symbol.name()));
  }

  private static class ConfigurationKey {
    private final NullabilityTarget target;
    private final NullabilityLevel level;

    ConfigurationKey(NullabilityTarget target, NullabilityLevel level) {
      this.target = target;
      this.level = level;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      ConfigurationKey that = (ConfigurationKey) o;

      if (target != that.target) return false;
      return level == that.level;
    }

    @Override
    public int hashCode() {
      int result = target.hashCode();
      result = 31 * result + level.hashCode();
      return result;
    }
  }

  private static class TypesForAnnotations extends ArrayList> {

    private NullabilityType getTypeFromAnnotation(AnnotationInstance annotation) {
      if (annotation.symbol().isUnknown()) {
        return NullabilityType.UNKNOWN;
      }
      for (Function typeForAnnotation : this) {
        NullabilityType type = typeForAnnotation.apply(annotation);
        if (type != NullabilityType.NO_ANNOTATION) {
          return type;
        }
      }
      return NullabilityType.NO_ANNOTATION;
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy