SonarQube Custom Rules: Write Your Own Code Analysis Rules

SonarQube Custom Rules: Write Your Own Code Analysis Rules

SonarQube ships with thousands of rules, but your team likely has conventions the built-in rules don't cover. Custom rules let you enforce team-specific patterns — banned methods, required annotations, naming conventions — using the SonarQube plugin API or simpler XPath/template rules.

Key Takeaways

Three ways to add custom rules. Template rules (no code), XPath rules (XML/XSD), and Java plugin (full AST analysis). Start with templates; write a plugin when you need real AST access.

The SonarQube plugin API gives you a syntax tree walker. You visit nodes in the AST and flag issues when a pattern matches.

Custom rules are deployed as JAR files. Build your plugin, drop it in $SONARQUBE_HOME/extensions/plugins/, restart.

Template rules work without code. Create them directly in the UI for simple patterns: required method calls, banned method names, regex-based checks.

Test your rules before deploying. The sonar-java-plugin-api includes a rule testing framework. Write unit tests for your rule implementations.

When to Write Custom Rules

Built-in SonarQube rules cover security vulnerabilities, common bugs, and general code quality. You need custom rules when:

  • Your team bans specific frameworks or APIs (e.g., "never use Thread.sleep in tests")
  • Your architecture requires specific patterns (e.g., "every @Service must have an @Transactional method")
  • You have domain-specific naming conventions (e.g., "repository classes must end in Repository")
  • You're enforcing migration rules (e.g., "don't use the old LegacyClient class")

Option 1: Template Rules (No Code Required)

Template rules are predefined rule patterns you configure in the SonarQube UI. Navigate to Quality Profiles → Rules → Add Custom Rule.

Available templates:

XPath rule (for XML/XSD files): Write an XPath expression. If it matches, report an issue.

Regex rule (for any language): Report an issue on any line matching the regex.

Example: Forbid Thread.sleep in test files:

Rule Template: Comment/Regex Rule on Java
Pattern: Thread\.sleep\(
Message: Do not use Thread.sleep in tests. Use Awaitility instead.
File pattern: **/*Test.java

No deployment needed — template rules are live immediately after saving.

Option 2: Writing a Java Plugin Rule

For AST-based rules that need real code understanding (not just text matching), write a plugin.

Project Setup

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.sonarsource.java</groupId>
        <artifactId>java-frontend</artifactId>
        <version>7.x.x</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.sonarsource.sonarqube</groupId>
        <artifactId>sonar-plugin-api</artifactId>
        <version>10.x.x</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Write the Rule

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.*;

@Rule(
    key = "NoSystemOutPrintln",
    name = "Use a logger instead of System.out.println",
    description = "System.out.println doesn't respect log levels and can't be disabled. Use SLF4J.",
    tags = {"logging", "convention"}
)
public class NoSystemOutPrintlnRule extends IssuableSubscriptionVisitor {

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

    @Override
    public void visitNode(Tree tree) {
        MethodInvocationTree mit = (MethodInvocationTree) tree;
        if (isSystemOutPrintln(mit)) {
            reportIssue(mit, "Replace System.out.println with a logger call.");
        }
    }

    private boolean isSystemOutPrintln(MethodInvocationTree mit) {
        ExpressionTree select = mit.methodSelect();
        if (select.is(Tree.Kind.MEMBER_SELECT)) {
            MemberSelectExpressionTree mset = (MemberSelectExpressionTree) select;
            String methodName = mset.identifier().name();
            if (!"println".equals(methodName) && !"print".equals(methodName)) {
                return false;
            }
            ExpressionTree object = mset.expression();
            if (object.is(Tree.Kind.MEMBER_SELECT)) {
                MemberSelectExpressionTree objectSelect = (MemberSelectExpressionTree) object;
                return "out".equals(objectSelect.identifier().name())
                    && "System".equals(objectSelect.expression().toString());
            }
        }
        return false;
    }
}

Register the Rule in a Plugin

@SonarLintSide
public class MyRulesPlugin implements Plugin {

    @Override
    public void define(Context context) {
        context.addExtensions(
            MyRulesDefinition.class,
            NoSystemOutPrintlnRule.class
        );
    }
}

public class MyRulesDefinition implements RulesDefinition {

    @Override
    public void define(Context context) {
        NewRepository repository = context
            .createRepository("my-company-rules", "java")
            .setName("My Company Rules");

        new AnnotationBasedRulesDefinition(repository, "java")
            .addRuleClasses(false, Arrays.asList(NoSystemOutPrintlnRule.class));

        repository.done();
    }
}

Test the Rule

import org.sonar.java.checks.verifier.CheckVerifier;

class NoSystemOutPrintlnRuleTest {

    @Test
    void detects_system_out_println() {
        CheckVerifier.newVerifier()
            .onFile("src/test/files/NoSystemOutPrintlnCheck.java")
            .withCheck(new NoSystemOutPrintlnRule())
            .verifyIssues();
    }
}

Test file (src/test/files/NoSystemOutPrintlnCheck.java):

class Example {
    void bad() {
        System.out.println("debug info"); // Noncompliant {{Replace System.out.println with a logger call.}}
        System.out.print("value"); // Noncompliant
    }

    void good() {
        log.info("debug info");
    }
}

Build and Deploy

mvn clean package
cp target/my-rules-plugin-1.0.0.jar $SONARQUBE_HOME/extensions/plugins/
# Restart SonarQube

After restart, your custom rule appears in Administration → Rules, filterable by repository "My Company Rules".

Activating Custom Rules

Custom rules aren't active by default. Add them to your quality profile:

Administration → Quality Profiles → [Your Profile] → Activate More → filter by repository "My Company Rules" → Activate

Or activate via REST API:

curl -X POST \
  "http://sonarqube/api/qualityprofiles/activate_rule" \
  -u admin:admin \
  -d "key=java-my-profile-12345&rule=my-company-rules:NoSystemOutPrintln&severity=MAJOR"

Advanced: Checking Method Contracts

A more complex rule: ensure every @Cacheable method has a corresponding @CacheEvict method in the same class:

@Rule(key = "CacheableWithoutEvict")
public class CacheableWithoutEvictRule extends IssuableSubscriptionVisitor {

    @Override
    public List<Tree.Kind> nodesToVisit() {
        return List.of(Tree.Kind.CLASS);
    }

    @Override
    public void visitNode(Tree tree) {
        ClassTree classTree = (ClassTree) tree;
        boolean hasCacheable = false;
        boolean hasCacheEvict = false;

        for (Tree member : classTree.members()) {
            if (member.is(Tree.Kind.METHOD)) {
                MethodTree method = (MethodTree) member;
                for (AnnotationTree annotation : method.modifiers().annotations()) {
                    String name = annotation.annotationType().toString();
                    if (name.contains("Cacheable")) hasCacheable = true;
                    if (name.contains("CacheEvict")) hasCacheEvict = true;
                }
            }
        }

        if (hasCacheable && !hasCacheEvict) {
            reportIssue(classTree.simpleName(),
                "Class uses @Cacheable but has no @CacheEvict method. Cache may grow unbounded.");
        }
    }
}

Custom rules turn SonarQube into your team's code review co-pilot — enforcing not just language-level best practices, but your specific architectural decisions.

Read more

Start now free