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.sleepin tests") - Your architecture requires specific patterns (e.g., "every
@Servicemust have an@Transactionalmethod") - 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
LegacyClientclass")
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.javaNo 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 SonarQubeAfter 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.