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

org.sonar.java.model.JSymbolMetadata 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.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;

import static org.sonar.java.model.JSymbolMetadataNullabilityHelper.getNullabilityDataAtLevel;

final class JSymbolMetadata implements SymbolMetadata {

  private static final NullabilityData[] NO_ANNOTATION_NULLABILITY =
    forEachLevel(level -> new JNullabilityData(NullabilityType.NO_ANNOTATION, level, null, null, false));

  private static final NullabilityData[] UNKNOWN_NULLABILITY =
    forEachLevel(level -> new JNullabilityData(NullabilityType.UNKNOWN, level, null, null, false));

  private final JSema sema;
  private final Symbol symbol;
  private final IAnnotationBinding[] annotationBindings;

  /**
   * Cache for {@link #annotations()}.
   */
  private List annotations;

  private final Map nullabilityCache = new EnumMap<>(NullabilityTarget.class);

  JSymbolMetadata(JSema sema, Symbol symbol, IAnnotationBinding[] annotationBindings) {
    this.sema = Objects.requireNonNull(sema);
    this.symbol = symbol;
    this.annotationBindings = annotationBindings;
  }

  JSymbolMetadata(JSema sema, Symbol symbol, IAnnotationBinding[] typeAnnotationBindings, IAnnotationBinding[] annotationBindings) {
    this.sema = Objects.requireNonNull(sema);
    this.symbol = symbol;
    this.annotationBindings = new IAnnotationBinding[typeAnnotationBindings.length + annotationBindings.length];
    System.arraycopy(typeAnnotationBindings, 0, this.annotationBindings, 0, typeAnnotationBindings.length);
    System.arraycopy(annotationBindings, 0, this.annotationBindings, typeAnnotationBindings.length, annotationBindings.length);
  }

  private static NullabilityData[] forEachLevel(Function initializer) {
    return Arrays.stream(NullabilityLevel.values()).map(initializer).toArray(NullabilityData[]::new);
  }

  public static NullabilityData noNullabilityAnnotationAt(NullabilityLevel level) {
    return NO_ANNOTATION_NULLABILITY[level.ordinal()];
  }

  public static NullabilityData unknownNullabilityAt(NullabilityLevel level) {
    return UNKNOWN_NULLABILITY[level.ordinal()];
  }

  @Override
  public List annotations() {
    if (annotations == null) {
      annotations = Arrays.stream(annotationBindings)
        .map(sema::annotation)
        .collect(Collectors.toList());
    }
    return annotations;
  }

  @Override
  public final boolean isAnnotatedWith(String fullyQualifiedName) {
    for (AnnotationInstance a : annotations()) {
      if (a.symbol().type().is(fullyQualifiedName)) {
        return true;
      }
    }
    return false;
  }

  @Nullable
  @Override
  public final List valuesForAnnotation(String fullyQualifiedNameOfAnnotation) {
    for (AnnotationInstance a : annotations()) {
      if (a.symbol().type().is(fullyQualifiedNameOfAnnotation)) {
        // TODO what about repeating annotations?
        return a.values();
      }
    }
    return null;
  }

  @Override
  public NullabilityData nullabilityData() {
    NullabilityTarget target = getTarget(symbol);
    if (target == null) {
      return unknownNullabilityAt(NullabilityLevel.UNKNOWN);
    }
    return nullabilityData(target);
  }

  @Override
  public NullabilityData nullabilityData(NullabilityTarget target) {
    return nullabilityCache.computeIfAbsent(target, this::resolveNullability);
  }

  @Nullable
  @Override
  public AnnotationTree findAnnotationTree(AnnotationInstance annotationInstance) {
    Tree declaration = symbol.declaration();
    if (declaration != null) {
      return findAnnotationTree(ModifiersUtils.getAnnotations(declaration), annotationInstance);
    }
    return null;
  }

  @Nullable
  private static AnnotationTree findAnnotationTree(List annotations, AnnotationInstance annotationInstance) {
    for (AnnotationTree annotationTree : annotations) {
      if (annotationTree.symbolType().symbol() == annotationInstance.symbol() &&
        annotationTree.arguments().size() == annotationInstance.values().size()) {
        return annotationTree;
      }
    }
    return null;
  }

  private NullabilityData resolveNullability(NullabilityTarget target) {
    NullabilityLevel currentLevel = getLevel(symbol);
    NullabilityData nullabilityDataAtLevel = getNullabilityDataAtLevel(this, target, currentLevel);
    if (nullabilityDataAtLevel.type() != NullabilityType.NO_ANNOTATION) {
      return nullabilityDataAtLevel;
    }

    // Check nullability from the inheritance hierarchy
    if (symbol.isMethodSymbol()) {
      NullabilityData nullabilityDataFromInheritance = getNullabilityDataFromInheritance((Symbol.MethodSymbol) symbol, target);
      if (nullabilityDataFromInheritance.type() != NullabilityType.NO_ANNOTATION) {
        return nullabilityDataFromInheritance;
      }
    }

    // Not annotated or meta annotated, check upper level in the ownership hierarchy...
    if (symbol.isPackageSymbol()) {
      return NO_ANNOTATION_NULLABILITY[currentLevel.ordinal()];
    }
    Symbol owner = getEffectiveOwner(symbol, currentLevel);
    return owner == null ? unknownNullabilityAt(currentLevel) : owner.metadata().nullabilityData(target);
  }

  private static NullabilityData getNullabilityDataFromInheritance(Symbol.MethodSymbol methodSymbol, NullabilityTarget target) {
    List overridenSymbols = methodSymbol.overriddenSymbols();
    NullabilityLevel level = NullabilityLevel.METHOD;
    for (Symbol.MethodSymbol overridenSymbol: overridenSymbols) {
      SymbolMetadata metadata = overridenSymbol.metadata();
      NullabilityData nullabilityData = getNullabilityDataAtLevel(metadata, target, level);
      if (nullabilityData.type() != NullabilityType.NO_ANNOTATION && !nullabilityData.equals(unknownNullabilityAt(level))) {
        return nullabilityData;
      }
    }
    return NO_ANNOTATION_NULLABILITY[level.ordinal()];
  }

  @CheckForNull
  private static Symbol getEffectiveOwner(Symbol symbol, NullabilityLevel currentLevel) {
    Symbol owner = symbol.owner();
    if (owner != null && owner.isMethodSymbol() && currentLevel == NullabilityLevel.CLASS) {
      // Owner of anonymous class can be a method symbol, but the nullability of the method is unrelated to the ones from the anonymous classes.
      return getEffectiveOwner(owner, currentLevel);
    }
    return owner;
  }

  private static NullabilityLevel getLevel(Symbol symbol) {
    if (symbol.isVariableSymbol()) {
      return NullabilityLevel.VARIABLE;
    } else if (symbol.isMethodSymbol()) {
      return NullabilityLevel.METHOD;
    } else if (symbol.isTypeSymbol()) {
      return NullabilityLevel.CLASS;
    } else if (symbol.isPackageSymbol()) {
      return NullabilityLevel.PACKAGE;
    }
    return NullabilityLevel.UNKNOWN;
  }

  @CheckForNull
  private static NullabilityTarget getTarget(Symbol symbol) {
    if (symbol.isMethodSymbol()) {
      return NullabilityTarget.METHOD;
    } else if (symbol.isVariableSymbol()) {
      Symbol owner = symbol.owner();
      if (owner != null && owner.isTypeSymbol()) {
        return NullabilityTarget.FIELD;
      }
      Tree variableTree = symbol.declaration();
      if (variableTree == null) {
        return NullabilityTarget.PARAMETER;
      }
      Tree variableTreeParent = variableTree.parent();
      if (variableTreeParent == null || variableTreeParent instanceof MethodTree) {
        return NullabilityTarget.PARAMETER;
      }
      // If variableTreeParent is not a lambda, we consider that the target is a local variable.
      if (!variableTreeParent.is(Tree.Kind.LAMBDA_EXPRESSION)) {
        return NullabilityTarget.LOCAL_VARIABLE;
      }
      // When variableTreeParent is an instance of LambdaExpressionTree we intentionally return null
      // because parameters of lambdas are not supported.
    }
    return null;
  }

  static final class JNullabilityData implements NullabilityData {

    private final NullabilityType type;
    private final NullabilityLevel level;

    @Nullable
    private final AnnotationInstance annotation;
    @Nullable
    private final AnnotationTree annotationTree;

    private final boolean metaAnnotation;

    public JNullabilityData(NullabilityType type, NullabilityLevel level, @Nullable AnnotationInstance annotation,
      @Nullable AnnotationTree annotationTree, boolean metaAnnotation) {
      this.type = type;
      this.level = level;
      this.annotation = annotation;
      this.annotationTree = annotationTree;
      this.metaAnnotation = metaAnnotation;
    }

    @Override
    public NullabilityType type() {
      return type;
    }

    @Override
    public NullabilityLevel level() {
      return level;
    }

    @Override
    public boolean metaAnnotation() {
      return metaAnnotation;
    }

    @Override
    public boolean isNonNull(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue) {
      return testNullabilityType(minLevel, ignoreMetaAnnotation, defaultValue, t -> t == NullabilityType.NON_NULL);
    }

    @Override
    public boolean isNullable(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue) {
      return testNullabilityType(minLevel, ignoreMetaAnnotation, defaultValue,
        t -> t == NullabilityType.STRONG_NULLABLE || t == NullabilityType.WEAK_NULLABLE);
    }

    @Override
    public boolean isStrongNullable(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue) {
      return testNullabilityType(minLevel, ignoreMetaAnnotation, defaultValue, t -> t == NullabilityType.STRONG_NULLABLE);
    }

    private boolean testNullabilityType(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue, Predicate typePredicate) {
      if (type == NullabilityType.NO_ANNOTATION) {
        return false;
      } else if (type == NullabilityType.UNKNOWN || (ignoreMetaAnnotation && metaAnnotation)) {
        return defaultValue;
      } else if (typePredicate.test(type)) {
        return minLevel.ordinal() <= level.ordinal();
      }
      return false;
    }

    @Nullable
    @Override
    public AnnotationInstance annotation() {
      return annotation;
    }

    @Nullable
    @Override
    public Tree declaration() {
      return annotationTree;
    }
  }

  static final class JAnnotationInstance implements AnnotationInstance {
    private final JSema sema;
    private final IAnnotationBinding annotationBinding;

    /**
     * Cache for {@link #values()}.
     */
    private List values;

    JAnnotationInstance(JSema sema, IAnnotationBinding annotationBinding) {
      this.sema = sema;
      this.annotationBinding = annotationBinding;
    }

    @Override
    public Symbol symbol() {
      return sema.typeSymbol(annotationBinding.getAnnotationType());
    }

    @Override
    public List values() {
      if (values == null) {
        values = Arrays.stream(annotationBinding.getDeclaredMemberValuePairs())
          .map(p -> new AnnotationValueImpl(p.getName(), convertAnnotationValue(p.getValue())))
          .collect(Collectors.toList());
      }
      return values;
    }

    private Object convertAnnotationValue(Object value) {
      if (value instanceof IVariableBinding) {
        return sema.variableSymbol((IVariableBinding) value);
      } else if (value instanceof ITypeBinding) {
        return sema.typeSymbol((ITypeBinding) value);
      } else if (value instanceof IAnnotationBinding) {
        return sema.annotation((IAnnotationBinding) value);
      } else if (value instanceof Object[]) {
        // Godin: probably better to not modify original array
        Object[] a = (Object[]) value;
        Object[] result = new Object[a.length];
        for (int i = 0; i < a.length; i++) {
          result[i] = convertAnnotationValue(a[i]);
        }
        return result;
      } else {
        return value;
      }
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy