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

org.sonar.java.checks.tests.AbstractJUnit5NotCompliantModifierChecker 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.tests;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.checks.helpers.UnitTestUtils;
import org.sonar.java.model.ModifiersUtils;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.reporting.JavaQuickFix;
import org.sonar.java.reporting.JavaTextEdit;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Modifier;
import org.sonar.plugins.java.api.tree.ModifierKeywordTree;
import org.sonar.plugins.java.api.tree.ModifiersTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

public abstract class AbstractJUnit5NotCompliantModifierChecker extends IssuableSubscriptionVisitor {

  public enum ModifierScope {
    // annotation like @Nested that applies to class
    CLASS,
    // annotation like @BeforeAll that applies to static class method
    CLASS_METHOD,
    // annotation like @BeforeEach that applies to non-static method
    INSTANCE_METHOD
  }

  protected static final String WRONG_MODIFIER_ISSUE_MESSAGE = "Remove this '%s' modifier.";

  protected abstract boolean isNonCompliantModifier(Modifier modifier, ModifierScope modifierScope);

  protected abstract void raiseIssueOnNonCompliantReturnType(MethodTree methodTree);

  protected void raiseIssueOnNonCompliantModifier(ModifierKeywordTree modifier) {
    QuickFixHelper.newIssue(context)
      .forRule(this)
      .onTree(modifier)
      .withMessage(WRONG_MODIFIER_ISSUE_MESSAGE, modifier.keyword().text())
      .withQuickFix(() ->
        JavaQuickFix.newQuickFix("Remove \"%s\" modifier", modifier.keyword().text())
          .addTextEdit(JavaTextEdit.removeTextSpan(AnalyzerMessage.textSpanBetween(modifier, true, QuickFixHelper.nextToken(modifier), false)))
        .build())
      .report();
  }

  @Override
  public List nodesToVisit() {
    return Collections.singletonList(Tree.Kind.CLASS);
  }

  @Override
  public void visitNode(Tree tree) {
    ClassTree classTree = (ClassTree) tree;
    if (classTree.symbol().isAbstract()) {
      return;
    }

    List junit5ClassMethods = new ArrayList<>();
    List junit5InstanceMethods = new ArrayList<>();
    List nonJunit5Methods = new ArrayList<>();
    classTree.members().stream()
      .filter(member -> member.is(Tree.Kind.METHOD))
      .map(MethodTree.class::cast)
      .forEach(method -> {
        if (UnitTestUtils.hasJUnit5TestAnnotation(method) || UnitTestUtils.hasJUnit5InstanceLifecycleAnnotation(method)) {
          if (isNotOverriding(method)) {
            junit5InstanceMethods.add(method);
          }
        } else if (UnitTestUtils.hasJUnit5ClassLifecycleAnnotation(method)) {
          if (isNotOverriding(method)) {
            junit5ClassMethods.add(method);
          }
        } else {
          nonJunit5Methods.add(method);
        }
      });

    raiseIssueOnMethods(junit5ClassMethods, ModifierScope.CLASS_METHOD);
    raiseIssueOnMethods(junit5InstanceMethods, ModifierScope.INSTANCE_METHOD);
    boolean classHasJunit5InstanceMethods = !junit5InstanceMethods.isEmpty();
    if (classHasJunit5InstanceMethods) {
      raiseIssueOnClass(nonJunit5Methods, classTree);
    }
  }

  private void raiseIssueOnMethods(List junit5ClassMethods, ModifierScope classMethod) {
    for (MethodTree junit5ClassMethod : junit5ClassMethods) {
      raiseIssueOnNotCompliantModifiers(junit5ClassMethod.modifiers(), classMethod);
      raiseIssueOnNonCompliantReturnType(junit5ClassMethod);
    }
  }

  private void raiseIssueOnClass(List nonJunit5Methods, ClassTree classTree) {
    boolean hasPublicStaticMethods = nonJunit5Methods.stream()
      .map(MethodTree::modifiers)
      .anyMatch(AbstractJUnit5NotCompliantModifierChecker::isPublicStatic);

    boolean hasPublicStaticFields = classTree.members().stream()
      .filter(member -> member.is(Tree.Kind.VARIABLE))
      .map(VariableTree.class::cast)
      .map(VariableTree::modifiers)
      .anyMatch(AbstractJUnit5NotCompliantModifierChecker::isPublicStatic);

    // Can we change the visibility of the class?
    if (hasPublicStaticMethods || hasPublicStaticFields) {
      return;
    }
    raiseIssueOnNotCompliantModifiers(classTree.modifiers(), ModifierScope.CLASS);
  }

  private static boolean isPublicStatic(ModifiersTree modifiers) {
    return ModifiersUtils.hasAll(modifiers, Modifier.PUBLIC, Modifier.STATIC);
  }

  private void raiseIssueOnNotCompliantModifiers(ModifiersTree modifierTree, ModifierScope modifierScope) {
    modifierTree.modifiers().stream()
      .filter(modifier -> isNonCompliantModifier(modifier.modifier(), modifierScope))
      .findFirst()
      .ifPresent(this::raiseIssueOnNonCompliantModifier);
  }

  private static boolean isNotOverriding(MethodTree tree) {
    // When it cannot be decided, isOverriding will return null, we consider that it as an override to avoid FP.
    return Boolean.FALSE.equals(tree.isOverriding());
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy