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

org.sonar.java.checks.spring.NonSingletonAutowiredInSingletonCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube Java
 * Copyright (C) 2012-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * 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 Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.java.checks.spring;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.MethodTreeUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
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.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S6832")
public class NonSingletonAutowiredInSingletonCheck extends IssuableSubscriptionVisitor {
  private static final String SCOPED_ANNOTATION = "org.springframework.context.annotation.Scope";
  private static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired";
  private static final String JAVAX_INJECT_ANNOTATION = "javax.inject.Inject";
  private static final String JAKARTA_INJECT_ANNOTATION = "jakarta.inject.Inject";
  private static final Set AUTO_WIRING_ANNOTATIONS = Set.of(AUTOWIRED_ANNOTATION, JAVAX_INJECT_ANNOTATION, JAKARTA_INJECT_ANNOTATION);

  @Override
  public List nodesToVisit() {
    return List.of(Tree.Kind.ANNOTATION, Tree.Kind.CONSTRUCTOR);
  }

  /**
   * This rule reports an issue when a Singleton Bean auto-wires a non-Singleton Bean via constructor, field or method parameter.
   */
  @Override
  public void visitNode(Tree tree) {
    if (tree.is(Tree.Kind.ANNOTATION)) {
      analyzeAnnotation((AnnotationTree) tree);
    }

    if (tree.is(Tree.Kind.CONSTRUCTOR)) {
      analyzeSingleArgumentConstructor((MethodTree) tree);
    }
  }

  private void analyzeAnnotation(AnnotationTree annotationTree) {
    if (!isAutoWiringAnnotation(annotationTree)) {
      return;
    }

    var annotatedSymbol = Optional.ofNullable(annotationTree.parent()).map(Tree::parent).orElse(null);
    if (annotatedSymbol == null) {
      return;
    }

    if (annotatedSymbol.is(Tree.Kind.VARIABLE)) {
      analyzeAnnotatedFieldOrParameter((VariableTree) annotatedSymbol);

    } else if (annotatedSymbol.is(Tree.Kind.METHOD)) {
      analyzeAnnotatedSetter((MethodTree) annotatedSymbol);

    } else if (annotatedSymbol.is(Tree.Kind.CONSTRUCTOR)) {
      analyzeAnnotatedConstructor((MethodTree) annotatedSymbol);
    }
  }

  private void analyzeAnnotatedFieldOrParameter(VariableTree annotatedVar) {
    String injectionType;

    if (isClassField(annotatedVar)) {
      injectionType = "autowired field";
    } else if (isSetterParameter(annotatedVar) || isConstructorParameter(annotatedVar)) {
      injectionType = "autowired parameter";
    } else {
      injectionType = null;
    }

    if (injectionType != null) {
      getEnclosingClass(annotatedVar.symbol().enclosingClass())
        .ifPresent(enclosingClassTree -> reportIfNonSingletonInSingleton(enclosingClassTree, annotatedVar, injectionType));
    }
  }

  private void analyzeAnnotatedSetter(MethodTree annotatedMethod) {
    if (MethodTreeUtils.isSetterMethod(annotatedMethod)) {
      getEnclosingClass(annotatedMethod.symbol().enclosingClass())
        .ifPresent(enclosingClassTree -> annotatedMethod.parameters()
          .forEach(variableTree -> reportIfNonSingletonInSingleton(enclosingClassTree, variableTree, "autowired setter method")));
    }
  }

  private void analyzeAnnotatedConstructor(MethodTree annotatedConstructor) {
    getEnclosingClass(annotatedConstructor.symbol().enclosingClass())
      .ifPresent(enclosingClassTree -> annotatedConstructor.parameters()
        .forEach(variableTree -> reportIfNonSingletonInSingleton(enclosingClassTree, variableTree, "autowired constructor")));
  }

  private void analyzeSingleArgumentConstructor(MethodTree constructorTree) {
    if (constructorTree.parameters().size() == 1) {
      var constructorParameter = constructorTree.parameters().get(0);
      getEnclosingClass(constructorTree.symbol().enclosingClass())
        .ifPresent(enclosingClassTree -> reportIfNonSingletonInSingleton(enclosingClassTree, constructorParameter, "single argument constructor"));
    }
  }

  private static boolean isClassField(VariableTree variableTree) {
    return Optional.ofNullable(variableTree.parent())
      .filter(parent -> parent.is(Tree.Kind.CLASS))
      .isPresent();
  }

  private static boolean isSetterParameter(VariableTree variableTree) {
    return Optional.ofNullable(variableTree.parent())
      .filter(parent -> parent.is(Tree.Kind.METHOD))
      .map(MethodTree.class::cast)
      .filter(MethodTreeUtils::isSetterMethod)
      .isPresent();
  }

  private static boolean isConstructorParameter(VariableTree variableTree) {
    return Optional.ofNullable(variableTree.parent())
      .filter(parent -> parent.is(Tree.Kind.CONSTRUCTOR))
      .isPresent();
  }

  private static Optional getEnclosingClass(@Nullable Symbol.TypeSymbol enclosingClassSymbol) {
    return Optional.ofNullable(enclosingClassSymbol).map(Symbol::declaration).map(ClassTree.class::cast);
  }

  private void reportIfNonSingletonInSingleton(ClassTree enclosingClassTree, VariableTree variableTree, String injectionType) {
    if (isSingletonBean(enclosingClassTree) && hasTypeNotSingletonBean(variableTree)) {
      reportIssue(variableTree.type(), "Don't auto-wire this non-Singleton bean into a Singleton bean (" + injectionType + ").");
    }
  }

  private static boolean hasTypeNotSingletonBean(VariableTree variableTree) {
    return hasNotSingletonScopeAnnotation(variableTree.symbol().type().symbol().metadata().annotations());
  }

  private static boolean isAutoWiringAnnotation(AnnotationTree annotationTree) {
    return AUTO_WIRING_ANNOTATIONS.contains(annotationTree.symbolType().fullyQualifiedName());
  }

  private static boolean isSingletonBean(ClassTree classTree) {
    return !hasNotSingletonScopeAnnotation(classTree.symbol().metadata().annotations());
  }

  private static boolean hasNotSingletonScopeAnnotation(List annotations) {
    // Only classes annotated with @Scope, having a value different from "singleton", are considered as non-Singleton
    return annotations.stream().anyMatch(NonSingletonAutowiredInSingletonCheck::isNotSingletonScopeAnnotation);
  }

  private static boolean isNotSingletonScopeAnnotation(SymbolMetadata.AnnotationInstance annotationInstance) {
    return annotationInstance.symbol().type().is(SCOPED_ANNOTATION)
      && annotationInstance.values()
        .stream()
        .anyMatch(NonSingletonAutowiredInSingletonCheck::isNotSingletonAnnotationValue);
  }

  private static boolean isNotSingletonAnnotationValue(SymbolMetadata.AnnotationValue annotationValue) {
    return ("value".equals(annotationValue.name()) || "scopeName".equals(annotationValue.name()))
      // both "value" and "scopeName" in @Scope annotation have String type
      && !"singleton".equalsIgnoreCase((String) annotationValue.value());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy