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

org.sonar.java.checks.design.SingletonUsageCheck 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.design;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ExpressionsHelper;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.EnumConstantTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
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 = "S6548")
public class SingletonUsageCheck extends IssuableSubscriptionVisitor {
  private static final String MESSAGE = "A Singleton implementation was detected." + " " +
    "Make sure the use of the Singleton pattern is required and the implementation is the right one for the context.";
  private static final String MESSAGE_FOR_ENUMS = "An Enum-based Singleton implementation was detected." + " " +
    "Make sure the use of the Singleton pattern is required and an Enum-based implementation is the right one for the context.";

  @Override
  public List nodesToVisit() {
    return Arrays.asList(Tree.Kind.CLASS, Tree.Kind.ENUM);
  }

  @Override
  public void visitNode(Tree tree) {
    final var classTree = (ClassTree) tree;
    if (tree.is(Tree.Kind.CLASS)) {
      visitClass(classTree);
    } else {
      visitEnum(classTree);
    }
  }

  private void visitEnum(ClassTree classTree) {
    var enumConstants = classTree.members().stream().filter(member -> member.is(Tree.Kind.ENUM_CONSTANT)).toList();
    if (enumConstants.size() == 1) {
      EnumConstantTree constant = (EnumConstantTree) enumConstants.get(0);
      if (isInitializedWithParameterFreeConstructor(constant) &&
        hasNonPrivateInstanceMethodsOrFields(classTree)) {
        reportIssue(classTree.simpleName(), MESSAGE_FOR_ENUMS,
          Collections.singletonList(new JavaFileScannerContext.Location("Single enum", constant)), null);
      }
    }
  }

  private void visitClass(ClassTree classTree) {
    var classAndInstance = collectClassAndField(classTree);

    if (classAndInstance == null) return;

    ClassTree singletonClass = classAndInstance.getKey();
    VariableTree singletonField = classAndInstance.getValue();

    var allConstructors = singletonClass.members().stream()
      .filter(member -> member.is(Tree.Kind.CONSTRUCTOR))
      .map(MethodTree.class::cast)
      .toList();

    if (allConstructors.size() <= 1 &&
      allConstructors.stream().allMatch(constructor -> constructor.symbol().isPrivate() && constructor.parameters().isEmpty()) &&
      hasNonPrivateInstanceMethodsOrFields(singletonClass)) {

      var flows = new ArrayList();
      flows.add(new JavaFileScannerContext.Location("Singleton field", singletonField.simpleName()));
      if (singletonClass != classTree) {
        flows.add(new JavaFileScannerContext.Location("Singleton helper", classTree.simpleName()));
      }
      allConstructors.forEach(constructor -> {
        IdentifierTree methodName = allConstructors.get(0).simpleName();
        flows.add(new JavaFileScannerContext.Location("Private constructor", methodName));
      });
      extractAssignments(singletonField).forEach(assignment -> flows.add(new JavaFileScannerContext.Location("Value assignment", assignment)));

      reportIssue(singletonClass.simpleName(), MESSAGE, flows, null);
    }
  }

  @CheckForNull
  private static Map.Entry collectClassAndField(ClassTree classTree) {
    ClassTree wrappingClass = null;
    final var parent = classTree.parent();
    if (parent != null && parent.is(Tree.Kind.CLASS)) {
      wrappingClass = (ClassTree) parent;
    }

    List staticFields = collectStaticFields(classTree, wrappingClass);
    if (staticFields.size() != 1) return null;

    var field = staticFields.get(0);

    final var fieldSymbol = field.symbol();
    ClassTree singletonClass = null;
    if (fieldSymbol.type().equals(classTree.symbol().type())) {
      singletonClass = classTree;
    } else {
      singletonClass = wrappingClass;
    }

    if (!isEffectivelyFinal(fieldSymbol)) return null;

    return new AbstractMap.SimpleEntry<>(singletonClass, field);
  }

  private static List collectStaticFields(ClassTree classTree, @Nullable ClassTree wrappingClass) {
    Type type = classTree.symbol().type();
    Type wrappingType = wrappingClass != null ? wrappingClass.symbol().type() : null;
    return classTree.members().stream()
      .filter(member -> member.is(Tree.Kind.VARIABLE) && ((VariableTree) member).symbol().isStatic())
      .map(VariableTree.class::cast)
      .filter(field -> {
        Type fieldType = field.symbol().type();
        return fieldType.equals(type) || (wrappingType != null && fieldType.equals(wrappingType));
      }).toList();
  }

  private static boolean isEffectivelyFinal(Symbol symbol) {
    return symbol.isFinal() ||
      (symbol.isPrivate() && ExpressionsHelper.getSingleWriteUsage(symbol) != null);
  }

  private static boolean isInitializedWithParameterFreeConstructor(EnumConstantTree constant) {
    return constant.initializer().methodSymbol().parameterTypes().isEmpty();
  }

  private static boolean hasNonPrivateInstanceMethodsOrFields(ClassTree classTree) {
    return classTree.members().stream().anyMatch(member -> {
      if (member.is(Tree.Kind.METHOD)) {
        var symbol = ((MethodTree) member).symbol();
        return !symbol.isPrivate() && !symbol.isStatic();
      } else if (member.is(Tree.Kind.VARIABLE)) {
        var symbol = ((VariableTree) member).symbol();
        return !symbol.isPrivate() && !symbol.isStatic();
      } else {
        return false;
      }
    });
  }

  private static List extractAssignments(VariableTree variable) {
    return variable.symbol().usages().stream()
      .map(Tree::parent)
      .filter(usage -> usage.is(Tree.Kind.ASSIGNMENT))
      .map(AssignmentExpressionTree.class::cast)
      .toList();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy