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

org.sonar.java.checks.tests.MockitoAnnotatedObjectsShouldBeInitializedCheck Maven / Gradle / Ivy

There is a newer version: 8.10.0.38194
Show newest version
/*
 * SonarQube Java
 * Copyright (C) 2012-2024 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.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
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.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

import static org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationInstance;

@Rule(key = "S5979")
public class MockitoAnnotatedObjectsShouldBeInitializedCheck extends IssuableSubscriptionVisitor {
  private static final List TARGET_ANNOTATIONS = Arrays.asList(
    "org.mockito.Captor",
    "org.mockito.InjectMocks",
    "org.mockito.Mock",
    "org.mockito.Spy"
  );

  private static final String EXTEND_WITH_ANNOTATION = "org.junit.jupiter.api.extension.ExtendWith";
  private static final String RUN_WITH_ANNOTATION = "org.junit.runner.RunWith";

  private static final List BEFORE_ANNOTATIONS = Arrays.asList(
    "org.junit.Before",
    "org.junit.jupiter.api.BeforeEach"
  );

  private static final String RULE_ANNOTATION = "org.junit.Rule";

  private static final MethodMatchers MOCKITO_JUNIT_RULE = MethodMatchers.create()
    .ofSubTypes("org.mockito.junit.MockitoJUnit")
    .names("rule")
    .addWithoutParametersMatcher()
    .build();

  private static final String MESSAGE = "Initialize mocks before using them.";

  private final Set coveredByExtendWithAnnotation = new HashSet<>();

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

  @Override
  public void visitNode(Tree tree) {
    ClassTree testClass = (ClassTree) tree;
    if (coveredByExtendWithAnnotation.contains(testClass)) {
      return;
    }

    if (isMetaAnnotated(testClass.symbol(), EXTEND_WITH_ANNOTATION, new HashSet<>())) {
      List classes = getInnerClassesCoveredByAnnotation(testClass);
      coveredByExtendWithAnnotation.addAll(classes);
      return;
    }

    List mocksToInitialize = testClass.members().stream()
      .filter(MockitoAnnotatedObjectsShouldBeInitializedCheck::isFieldWithTargetAnnotation)
      .map(VariableTree.class::cast)
      .toList();

    if (!mocksToInitialize.isEmpty() && !mocksAreProperlyInitialized(testClass)) {
      AnnotationTree firstAnnotation = mocksToInitialize.get(0).modifiers().annotations().get(0);
      reportIssue(firstAnnotation, MESSAGE);
    }
  }

  @Override
  public void leaveFile(JavaFileScannerContext context) {
    super.leaveFile(context);
    coveredByExtendWithAnnotation.clear();
  }

  private static boolean isMetaAnnotated(Symbol symbol, String annotation, Set visited) {
    if (visited.contains(symbol)) {
      return false;
    }
    for (AnnotationInstance a : symbol.metadata().annotations()) {
      visited.add(symbol);
      if (a.symbol().type().is(annotation) || isMetaAnnotated(a.symbol(), annotation, visited)) {
        return true;
      }
    }
    return false;
  }

  private static boolean hasAnnotation(ClassTree tree, String annotation) {
    return tree.symbol().metadata().isAnnotatedWith(annotation);
  }

  private static boolean isFieldWithTargetAnnotation(Tree tree) {
    if (!tree.is(Tree.Kind.VARIABLE)) {
      return false;
    }
    VariableTree field = (VariableTree) tree;
    SymbolMetadata metadata = field.symbol().metadata();
    return TARGET_ANNOTATIONS.stream().anyMatch(metadata::isAnnotatedWith);
  }

  private static boolean mocksAreProperlyInitialized(ClassTree testClass) {
    return hasAnnotation(testClass, RUN_WITH_ANNOTATION) ||
      isMockitoJUnitRuleInvoked(testClass) ||
      areMocksInitializedInSetup(testClass);
  }


  private static List getInnerClassesCoveredByAnnotation(ClassTree tree) {
    NestedClassesCollector collector = new NestedClassesCollector();
    tree.accept(collector);
    return collector.classes;
  }

  private static boolean isMockitoJUnitRuleInvoked(ClassTree clazz) {
    List collected = clazz.members().stream()
      .filter(member -> member.is(Tree.Kind.VARIABLE))
      .map(VariableTree.class::cast)
      .toList();
    for (VariableTree field : collected) {
      if (field.type().symbolType().is("org.mockito.junit.MockitoRule")) {
        ExpressionTree initializer = field.initializer();
        if (initializer != null && initializer.is(Tree.Kind.METHOD_INVOCATION) &&
          field.symbol().metadata().isAnnotatedWith(RULE_ANNOTATION) &&
          isInitializedWithRule((MethodInvocationTree) initializer)) {
          return true;
        }
      }
    }
    return false;
  }

  private static boolean isInitializedWithRule(MethodInvocationTree mit) {
    MethodInvocationTree current = mit;
    while (true) {
      if (MOCKITO_JUNIT_RULE.matches(current)) {
        return true;
      }
      ExpressionTree expressionTree = current.methodSelect();
      if (!expressionTree.is(Tree.Kind.MEMBER_SELECT)) {
        return false;
      }
      ExpressionTree expression = ((MemberSelectExpressionTree) expressionTree).expression();
      if (!expression.is(Tree.Kind.METHOD_INVOCATION)) {
        return false;
      }
      current = (MethodInvocationTree) expression;
    }
  }

  private static boolean areMocksInitializedInSetup(ClassTree clazz) {
    List methods = getSetupMethods(clazz);
    for (MethodTree method : methods) {
      SetupMethodVisitor visitor = new SetupMethodVisitor();
      method.accept(visitor);
      if (visitor.initMocksIsInvoked) {
        return true;
      }
    }
    return hasParentClass(clazz);
  }

  private static boolean hasParentClass(ClassTree tree) {
    return tree.superClass() != null;
  }

  private static List getSetupMethods(ClassTree tree) {
    return tree.members().stream()
      .filter(member -> member.is(Tree.Kind.METHOD))
      .map(MethodTree.class::cast)
      .filter(MockitoAnnotatedObjectsShouldBeInitializedCheck::isTaggedWithBefore)
      .toList();
  }

  private static boolean isTaggedWithBefore(MethodTree method) {
    SymbolMetadata metadata = method.symbol().metadata();
    return BEFORE_ANNOTATIONS.stream().anyMatch(metadata::isAnnotatedWith);
  }


  /**
   * Traverses a tree looking for classes annotated with JUnit5's Nested
   */
  static class NestedClassesCollector extends BaseTreeVisitor {
    private static final String NESTED_ANNOTATION = "org.junit.jupiter.api.Nested";
    private final List classes = new ArrayList<>();

    @Override
    public void visitClass(ClassTree tree) {
      if (tree.symbol().metadata().isAnnotatedWith(NESTED_ANNOTATION)) {
        classes.add(tree);
      }
      tree.members().stream()
        .filter(member -> member.is(Tree.Kind.CLASS))
        .map(ClassTree.class::cast)
        .forEach(child -> child.accept(this));
    }
  }

  /**
   * Traverses a tree looking for an invocation of org.mockito.MockitoAnnotations.initMocks
   */
  private static class SetupMethodVisitor extends BaseTreeVisitor {
    private static final MethodMatchers OPEN_OR_INIT_MOCKS = MethodMatchers.create()
      .ofTypes("org.mockito.MockitoAnnotations")
      .names("openMocks", "initMocks")
      .addParametersMatcher("java.lang.Object")
      .build();

    private boolean initMocksIsInvoked = false;

    @Override
    public void visitMethodInvocation(MethodInvocationTree tree) {
      if (OPEN_OR_INIT_MOCKS.matches(tree)) {
        initMocksIsInvoked = true;
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy