Java custom rule is not working

Hi all,
I’m trying to write a new java custom rule for SonarQube (version 7.9.1).
The rule needs to check if the method names comply with two different regexes given as parameters.
If the rule is annotated with “Test” then one regex will be used to test the name, if it is not annotated then the other regex will be used.

I wrote the rule based on this documentation: https://docs.sonarqube.org/display/PLUG/Writing+Custom+Java+Rules+101.

When I run the unit tests for the rule it works as expected.
The problem appears when I try using the rule in SonarQube because the issues are raised for the test methods using the regex for the methods not annotated with “Test”. I used for the unit test the exact class that I used in the project analyzed in SonarQube and the issues are raised differently.

Hi,

In order to help you out, could you please share the code of your custom rule ? The intent seems quite well explained, but without the code it will be very hard to understand where the issue might come from.

Thanks

Hi,

I started with the java custom rule template from here: https://github.com/SonarSource/sonar-custom-rules-examples/tree/master/java-custom-rules.
In package org.sonar.samples.java.checks I added a new class called BadMethodNameCheck:

import static java.util.Arrays.asList;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "BadMethodName")
public class BadMethodNameCheck extends IssuableSubscriptionVisitor {

    private static final String DEFAULT_FORMAT = "^[a-z][a-zA-Z0-9]*$";
    private static final String DEFAULT_TEST_FORMAT = "^[a-z][a-zA-Z0-9]*_[a-zA-Z0-9]*_[a-zA-Z0-9]*$";

    @RuleProperty(key = "format", description = "Regular expression used to check the method names against.", defaultValue = ""
            + DEFAULT_FORMAT)
    public String format = DEFAULT_FORMAT;

    @RuleProperty(key = "testFormat", description = "Regular expression used to check the test method names against.", defaultValue = ""
            + DEFAULT_TEST_FORMAT)
    public String testFormat = DEFAULT_TEST_FORMAT;

    private Pattern pattern = null;
    private Pattern testPattern = null;

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

    @Override
    public void setContext(JavaFileScannerContext context) {
        if (pattern == null) {
            pattern = Pattern.compile(format, Pattern.DOTALL);
        }
        if (testPattern == null) {
            testPattern = Pattern.compile(testFormat, Pattern.DOTALL);
        }
        super.setContext(context);
    }

    @Override
    public void visitNode(Tree tree) {
        MethodTree method = (MethodTree) tree;
        if (hasTestAnnotation(method)) {
            if (isNotOverriden(method) && !testPattern.matcher(method.simpleName().name()).matches()) {
                reportIssue(method.simpleName(),
                        "Rename this test method name to match the regular expression '" + testFormat + "'.");
            }
        } else {
            if (isNotOverriden(method) && !pattern.matcher(method.simpleName().name()).matches()) {
                reportIssue(method.simpleName(),
                        "Rename this method name to match the regular expression '" + format + "'.");
            }
        }
    }

    private static boolean isNotOverriden(MethodTree methodTree) {
        return Boolean.FALSE.equals(methodTree.isOverriding());
    }

    private static final Set<String> TEST_ANNOTATIONS = new HashSet<>(
            asList("org.junit.Test", "org.testng.annotations.Test"));
    private static final Set<String> JUNIT5_TEST_ANNOTATIONS = new HashSet<>(asList("org.junit.jupiter.api.Test",
            "org.junit.jupiter.api.RepeatedTest", "org.junit.jupiter.api.TestFactory",
            "org.junit.jupiter.api.TestTemplate", "org.junit.jupiter.params.ParameterizedTest"));
    private static final String NESTED_ANNOTATION = "org.junit.jupiter.api.Nested";

    public static boolean hasNestedAnnotation(ClassTree tree) {
        SymbolMetadata metadata = tree.symbol().metadata();
        return metadata.isAnnotatedWith(NESTED_ANNOTATION);
    }

    public static boolean hasTestAnnotation(MethodTree tree) {
        SymbolMetadata symbolMetadata = tree.symbol().metadata();
        return TEST_ANNOTATIONS.stream().anyMatch(symbolMetadata::isAnnotatedWith)
                || hasJUnit5TestAnnotation(symbolMetadata);
    }

    public static boolean hasJUnit5TestAnnotation(MethodTree tree) {
        return hasJUnit5TestAnnotation(tree.symbol().metadata());
    }

    private static boolean hasJUnit5TestAnnotation(SymbolMetadata symbolMetadata) {
        return JUNIT5_TEST_ANNOTATIONS.stream().anyMatch(symbolMetadata::isAnnotatedWith);
    }

}

And I added these files to test it:
BadMethodNameCheckTest.java:


import org.junit.Test;
import org.sonar.java.checks.verifier.JavaCheckVerifier;

public class BadMethodNameCheckTest {
    @Test
    public void test() {
        JavaCheckVerifier.verify("src/test/files/BadMethodNameCheck.java", new BadMethodNameCheck());
    }
}

BadMethodNameCheck.java:

import org.junit.Test;

class BadMethodNameCheck {
  public BadMethodNameCheck() {
  }

  void BadMethodName() { // Noncompliant [[sc=8;ec=21]] {{Rename this method name to match the regular expression '^[a-z][a-zA-Z0-9]*$'.}}
  }

  void goodMethodName() {
  }

  void methodTested_someScenario_expectedResult() {  // Noncompliant [[sc=8;ec=48]] {{Rename this method name to match the regular expression '^[a-z][a-zA-Z0-9]*$'.}}
  }
  
  @Test
  void methodTested_someScenario_expectedResult() {  
  }
      
  @Test
  void BadNameForTest() { // Noncompliant [[sc=8;ec=22]] {{Rename this test method name to match the regular expression '^[a-z][a-zA-Z0-9]*_[a-zA-Z0-9]*_[a-zA-Z0-9]*$'.}}
  }

  @Override
  void BadMethodNameButOverrides(){
  }

  @Deprecated
  void BadMethodName2() { // Noncompliant
  }

  public String toString() { //Overrides from object
    return "...";
  }
}

The unit test works, but in SonarQube it’s rising a code smell at every test method that says:

Rename this method name to match the regular expression '^[a-z][a-zA-Z0-9]*$'

Welcome :slight_smile:

there’s a rule with regex paramater you might use
Method names should comply with a naming convention
It might be able to combine those two regexes.
Could you provide more details about the regexes / naming conventions ?
Edit = overlooked the part with the Annotation and it seems your rule is already based
on https://github.com/SonarSource/sonar-java/blob/master/java-checks/src/main/java/org/sonar/java/checks/naming/BadMethodNameCheck.java

Gilbert

If things is working in the unit test and not in the analysis I would first focus on the analysis part : It sounds like the fact that the method is annotated with a test annotation is not properly detected.
Therefore : how are you doing your analysis (which scanner) ? what is the test classpath provided to the java analyzer ?

I suspect that somewhat the classpath is not properly provided and thus the annotation @Test is not resolved as junit annotation and the condition in your custom check to identify test method cannot be true.