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

org.sonar.java.checks.EmptyMethodsCheck Maven / Gradle / Ivy

/*
 * 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;

import java.util.Arrays;
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.model.LineUtils;
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.location.Position;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.BlockTree;
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.StatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.Tree.Kind;

@Rule(key = "S1186")
public class EmptyMethodsCheck extends IssuableSubscriptionVisitor {

  // Some methods may legitimately be left empty, e.g. methods annotated with org.aspectj.lang.annotation.Pointcut. We ignore them here.
  private static final String IGNORED_METHODS_ANNOTATION = "org.aspectj.lang.annotation.Pointcut";
  private static final String IGNORED_METHODS_ANNOTATION_UNQUALIFIED = "Pointcut";

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

  @Override
  public void visitNode(Tree tree) {
    ClassTree classTree = (ClassTree) tree;
    if (!ModifiersUtils.hasModifier(classTree.modifiers(), Modifier.ABSTRACT)) {
      List members = classTree.members();
      checkMethods(members);
      checkConstructors(members);
    }
  }

  private void checkMethods(List members) {
    members.stream()
      .filter(member -> member.is(Tree.Kind.METHOD))
      .map(MethodTree.class::cast)
      .filter(methodTree -> {
        var annotations = methodTree.modifiers().annotations();
        return annotations.isEmpty() || annotations.stream().noneMatch(EmptyMethodsCheck::isExceptedAnnotation);
      })
      .forEach(this::checkMethod);
  }

  /**
   * Returns true if the annotation indicates that the method body can legitimately be empty.
   */
  private static boolean isExceptedAnnotation(AnnotationTree annotationTree) {
    return annotationTree.symbolType().is(IGNORED_METHODS_ANNOTATION) ||
      (annotationTree.symbolType().isUnknown() && annotationTree.symbolType().name().equals(IGNORED_METHODS_ANNOTATION_UNQUALIFIED));
  }

  private void checkConstructors(List members) {
    List constructors = members.stream()
      .filter(member -> member.is(Tree.Kind.CONSTRUCTOR))
      .map(MethodTree.class::cast)
      .toList();
    if (constructors.size() == 1 && isPublicNoArgConstructor(constructors.get(0))) {
      // In case that there is only a single public default constructor with empty body, we raise an issue, as this is equivalent to not
      // defining a constructor at all and hence redundant.
      checkMethod(constructors.get(0));
    } else if(constructors.size() > 1) {
      // If there are several constructors, it may be valid to have a no-args constructor with an empty body. However, constructors that
      // take arguments should do something with those or say why they don't using a comment.
      constructors.stream()
        .filter(constructor -> !constructor.parameters().isEmpty())
        .forEach(this::checkMethod);
    }
  }

  private static boolean isPublicNoArgConstructor(MethodTree constructor) {
    return ModifiersUtils.hasModifier(constructor.modifiers(), Modifier.PUBLIC) && constructor.parameters().isEmpty();
  }

  private void checkMethod(MethodTree methodTree) {
    BlockTree block = methodTree.block();
    if (block != null && isEmpty(block) && !containsComment(block)) {
      QuickFixHelper.newIssue(context)
        .forRule(this)
        .onTree(methodTree.simpleName())
        .withMessage("Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.")
        .withQuickFix(() -> computeQuickFix(methodTree))
        .report();
    }
  }

  private static boolean isEmpty(BlockTree block) {
    List body = block.body();
    return body.isEmpty() || body.stream().allMatch(stmt -> stmt.is(Kind.EMPTY_STATEMENT));
  }

  private static boolean containsComment(BlockTree block) {
    return !block.closeBraceToken().trivias().isEmpty();
  }

  private static JavaQuickFix computeQuickFix(MethodTree method) {
    String commentFormat;
    if (LineUtils.startLine(method.block().openBraceToken()) == LineUtils.startLine(method.block().closeBraceToken())) {
      commentFormat = " /* TODO document why this %s is empty */ ";
    } else {
      String methodPadding = computePadding(method);
      commentFormat = "\n" + methodPadding + "  // TODO document why this %s is empty\n" + methodPadding;
    }

    String comment = String.format(commentFormat, method.is(Kind.CONSTRUCTOR) ? "constructor" : "method");

    AnalyzerMessage.TextSpan textSpan = AnalyzerMessage.textSpanBetween(
      method.block().openBraceToken(), false,
      method.block().closeBraceToken(), false
    );

    return JavaQuickFix.newQuickFix("Insert placeholder comment")
      .addTextEdit(JavaTextEdit.replaceTextSpan(textSpan, comment))
      .build();
  }

  private static String computePadding(MethodTree method) {
    int spaces = Position.startOf(method).columnOffset();
    // This loop and return call can be replaced with a call to " ".repeat(spaces) in Java 11
    StringBuilder padding = new StringBuilder("");
    for (int i = 0; i < spaces; i++) {
      padding.append(" ");
    }
    return padding.toString();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy