PMD Custom Rules in Java: Complete Implementation Guide

PMD is a powerful static code analysis tool that helps identify potential problems in Java code. While PMD comes with many built-in rules, creating custom rules allows you to enforce project-specific coding standards and patterns. Here's a comprehensive guide to implementing custom PMD rules in Java.

Project Setup

Dependencies

pom.xml

<properties>
<pmd.version>6.55.0</pmd.version>
<maven-pmd-plugin.version>3.20.0</maven-pmd-plugin.version>
</properties>
<dependencies>
<!-- PMD Core -->
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-core</artifactId>
<version>${pmd.version}</version>
</dependency>
<!-- PMD Java -->
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-java</artifactId>
<version>${pmd.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for fluent assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- PMD Maven Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>${maven-pmd-plugin.version}</version>
<configuration>
<rulesets>
<ruleset>custom-rules.xml</ruleset>
</rulesets>
<printFailingErrors>true</printFailingErrors>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Core Implementation

1. Base Rule Classes

AbstractCustomRule.java - Base class for all custom rules

package com.yourcompany.pmd.rules;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.*;
import java.util.regex.Pattern;
/**
* Base class for all custom PMD rules with common utilities.
*/
public abstract class AbstractCustomRule extends AbstractJavaRule {
protected static final PropertyDescriptor<Boolean> IGNORE_TEST_CLASSES =
PropertyFactory.booleanProperty("ignoreTestClasses")
.desc("Ignore test classes (names ending with Test, *Test, *Tests)")
.defaultValue(true)
.build();
protected static final PropertyDescriptor<String> EXCLUDED_PACKAGES =
PropertyFactory.stringProperty("excludedPackages")
.desc("Comma-separated list of packages to exclude from analysis")
.defaultValue("")
.build();
protected static final PropertyDescriptor<Integer> MAX_COMPLEXITY =
PropertyFactory.intProperty("maxComplexity")
.desc("Maximum allowed complexity threshold")
.defaultValue(10)
.build();
public AbstractCustomRule() {
definePropertyDescriptor(IGNORE_TEST_CLASSES);
definePropertyDescriptor(EXCLUDED_PACKAGES);
definePropertyDescriptor(MAX_COMPLEXITY);
}
@Override
public void apply(List<? extends Node> nodes, RuleContext ctx) {
for (Node node : nodes) {
if (node instanceof ASTCompilationUnit) {
ASTCompilationUnit compilationUnit = (ASTCompilationUnit) node;
// Check if we should skip this compilation unit
if (shouldSkipAnalysis(compilationUnit)) {
return;
}
visit(compilationUnit, ctx);
}
}
}
protected boolean shouldSkipAnalysis(ASTCompilationUnit compilationUnit) {
// Skip test classes if configured
if (getProperty(IGNORE_TEST_CLASSES) && isTestClass(compilationUnit)) {
return true;
}
// Skip excluded packages
String packageName = getPackageName(compilationUnit);
String excludedPackages = getProperty(EXCLUDED_PACKAGES);
if (!excludedPackages.isEmpty() && isPackageExcluded(packageName, excludedPackages)) {
return true;
}
return false;
}
protected boolean isTestClass(ASTCompilationUnit compilationUnit) {
String className = getClassName(compilationUnit);
return className != null && 
(className.endsWith("Test") || 
className.endsWith("Tests") || 
className.endsWith("TestCase"));
}
protected String getPackageName(ASTCompilationUnit compilationUnit) {
ASTPackageDeclaration packageDecl = compilationUnit.getFirstDescendantOfType(ASTPackageDeclaration.class);
return packageDecl != null ? packageDecl.getName() : "";
}
protected String getClassName(ASTCompilationUnit compilationUnit) {
ASTClassOrInterfaceDeclaration classDecl = 
compilationUnit.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class);
return classDecl != null ? classDecl.getName() : null;
}
protected boolean isPackageExcluded(String packageName, String excludedPackages) {
if (packageName == null || packageName.isEmpty()) {
return false;
}
String[] excluded = excludedPackages.split(",");
for (String excludedPackage : excluded) {
if (packageName.startsWith(excludedPackage.trim())) {
return true;
}
}
return false;
}
protected void addViolationWithMessage(Node node, String message, Object... args) {
String formattedMessage = String.format(message, args);
addViolation(data, node, formattedMessage);
}
protected boolean hasAnnotation(Node node, String annotationName) {
if (node instanceof ASTAnnotation) {
ASTAnnotation annotation = (ASTAnnotation) node;
return annotation.getAnnotationName().equals(annotationName);
}
return false;
}
protected boolean hasAnyAnnotation(Node node, String... annotationNames) {
for (String annotationName : annotationNames) {
if (hasAnnotation(node, annotationName)) {
return true;
}
}
return false;
}
protected List<ASTMethodDeclaration> getMethodDeclarations(ASTClassOrInterfaceDeclaration classDecl) {
return classDecl.findDescendantsOfType(ASTMethodDeclaration.class);
}
protected List<ASTFieldDeclaration> getFieldDeclarations(ASTClassOrInterfaceDeclaration classDecl) {
return classDecl.findDescendantsOfType(ASTFieldDeclaration.class);
}
protected boolean isPublic(ASTMethodDeclaration method) {
return method.isPublic();
}
protected boolean isPrivate(ASTMethodDeclaration method) {
return method.isPrivate();
}
protected boolean isProtected(ASTMethodDeclaration method) {
return method.isProtected();
}
protected int getMethodComplexity(ASTMethodDeclaration method) {
// Simplified complexity calculation - in practice, use CyclomaticComplexityVisitor
return method.findDescendantsOfType(ASTIfStatement.class).size() +
method.findDescendantsOfType(ASTWhileStatement.class).size() +
method.findDescendantsOfType(ASTForStatement.class).size() +
method.findDescendantsOfType(ASTSwitchStatement.class).size();
}
}

2. Naming Convention Rules

MethodNamingRule.java - Enforces method naming conventions

package com.yourcompany.pmd.rules.naming;
import com.yourcompany.pmd.rules.AbstractCustomRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.regex.Pattern;
/**
* Rule that enforces method naming conventions.
*/
public class MethodNamingRule extends AbstractCustomRule {
private static final PropertyDescriptor<Boolean> ALLOW_UNDERSCORES =
PropertyFactory.booleanProperty("allowUnderscores")
.desc("Allow underscores in method names")
.defaultValue(false)
.build();
private static final PropertyDescriptor<String> TEST_METHOD_PATTERN =
PropertyFactory.stringProperty("testMethodPattern")
.desc("Regex pattern for test method names")
.defaultValue("^test[A-Z][a-zA-Z0-9]*$|^[a-z][a-zA-Z0-9]*Test$")
.build();
private static final PropertyDescriptor<String> PRIVATE_METHOD_PATTERN =
PropertyFactory.stringProperty("privateMethodPattern")
.desc("Regex pattern for private method names")
.defaultValue("^[a-z][a-zA-Z0-9]*$")
.build();
public MethodNamingRule() {
definePropertyDescriptor(ALLOW_UNDERSCORES);
definePropertyDescriptor(TEST_METHOD_PATTERN);
definePropertyDescriptor(PRIVATE_METHOD_PATTERN);
}
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
String methodName = node.getName();
// Skip constructors
if (node.isConstructor()) {
return super.visit(node, data);
}
// Check naming convention based on method type and context
if (isTestMethod(node)) {
validateTestMethodName(node, methodName, data);
} else if (isPrivate(node)) {
validatePrivateMethodName(node, methodName, data);
} else {
validateStandardMethodName(node, methodName, data);
}
return super.visit(node, data);
}
private boolean isTestMethod(ASTMethodDeclaration method) {
// Check for @Test annotation
if (hasAnyAnnotation(method, "Test", "org.junit.Test", "org.junit.jupiter.api.Test")) {
return true;
}
// Check if method name starts with "test"
String methodName = method.getName();
return methodName.startsWith("test") && Character.isUpperCase(methodName.charAt(4));
}
private void validateTestMethodName(ASTMethodDeclaration method, String methodName, Object data) {
String patternStr = getProperty(TEST_METHOD_PATTERN);
Pattern pattern = Pattern.compile(patternStr);
if (!pattern.matcher(methodName).matches()) {
addViolationWithMessage(data, method, 
"Test method name '%s' does not match pattern: %s", 
methodName, patternStr);
}
}
private void validatePrivateMethodName(ASTMethodDeclaration method, String methodName, Object data) {
String patternStr = getProperty(PRIVATE_METHOD_PATTERN);
Pattern pattern = Pattern.compile(patternStr);
if (!pattern.matcher(methodName).matches()) {
addViolationWithMessage(data, method,
"Private method name '%s' should start with lowercase letter and match pattern: %s",
methodName, patternStr);
}
// Check for underscores if not allowed
if (!getProperty(ALLOW_UNDERSCORES) && methodName.contains("_")) {
addViolationWithMessage(data, method,
"Private method name '%s' should not contain underscores", methodName);
}
}
private void validateStandardMethodName(ASTMethodDeclaration method, String methodName, Object data) {
// Standard method naming: camelCase, starting with lowercase
if (!Character.isLowerCase(methodName.charAt(0))) {
addViolationWithMessage(data, method,
"Method name '%s' should start with a lowercase letter", methodName);
}
// Check for underscores if not allowed
if (!getProperty(ALLOW_UNDERSCORES) && methodName.contains("_")) {
addViolationWithMessage(data, method,
"Method name '%s' should not contain underscores", methodName);
}
// Check for consecutive uppercase letters (except for acronyms)
if (hasConsecutiveUppercase(methodName)) {
addViolationWithMessage(data, method,
"Method name '%s' should not have consecutive uppercase letters", methodName);
}
}
private boolean hasConsecutiveUppercase(String name) {
for (int i = 1; i < name.length(); i++) {
if (Character.isUpperCase(name.charAt(i)) && Character.isUpperCase(name.charAt(i - 1))) {
return true;
}
}
return false;
}
@Override
public String getName() {
return "CustomMethodNaming";
}
@Override
public String getMessage() {
return "Method naming convention violation";
}
@Override
public String getDescription() {
return "Enforces consistent method naming conventions across the codebase";
}
}

ConstantNamingRule.java - Enforces constant naming conventions

package com.yourcompany.pmd.rules.naming;
import com.yourcompany.pmd.rules.AbstractCustomRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.regex.Pattern;
/**
* Rule that enforces constant (static final) field naming conventions.
*/
public class ConstantNamingRule extends AbstractCustomRule {
private static final PropertyDescriptor<String> CONSTANT_PATTERN =
PropertyFactory.stringProperty("constantPattern")
.desc("Regex pattern for constant field names")
.defaultValue("^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$")
.build();
private static final PropertyDescriptor<Boolean> CHECK_STATIC_FINAL =
PropertyFactory.booleanProperty("checkStaticFinal")
.desc("Check only static final fields")
.defaultValue(true)
.build();
public ConstantNamingRule() {
definePropertyDescriptor(CONSTANT_PATTERN);
definePropertyDescriptor(CHECK_STATIC_FINAL);
}
@Override
public Object visit(ASTFieldDeclaration node, Object data) {
// Check if this is a constant (static final) field
if (isConstantField(node)) {
ASTVariableDeclarator declarator = node.getFirstDescendantOfType(ASTVariableDeclarator.class);
if (declarator != null) {
String fieldName = declarator.getName();
validateConstantName(node, fieldName, data);
}
}
return super.visit(node, data);
}
private boolean isConstantField(ASTFieldDeclaration field) {
if (getProperty(CHECK_STATIC_FINAL)) {
return field.isStatic() && field.isFinal();
}
return field.isFinal(); // Check all final fields if not restricted to static final
}
private void validateConstantName(ASTFieldDeclaration field, String fieldName, Object data) {
String patternStr = getProperty(CONSTANT_PATTERN);
Pattern pattern = Pattern.compile(patternStr);
if (!pattern.matcher(fieldName).matches()) {
addViolationWithMessage(data, field,
"Constant field name '%s' should be uppercase with underscores: %s",
fieldName, patternStr);
}
// Additional validations
if (fieldName.toLowerCase().equals(fieldName)) {
addViolationWithMessage(data, field,
"Constant field name '%s' should contain uppercase letters", fieldName);
}
if (fieldName.toUpperCase().equals(fieldName) && !fieldName.contains("_")) {
addViolationWithMessage(data, field,
"Constant field name '%s' should use underscores to separate words", fieldName);
}
}
@Override
public String getName() {
return "CustomConstantNaming";
}
@Override
public String getMessage() {
return "Constant naming convention violation";
}
@Override
public String getDescription() {
return "Enforces UPPER_CASE naming convention for constant fields";
}
}

3. Security Rules

HardcodedCredentialsRule.java - Detects hardcoded passwords and secrets

package com.yourcompany.pmd.rules.security;
import com.yourcompany.pmd.rules.AbstractCustomRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
/**
* Rule that detects hardcoded credentials and secrets in the code.
*/
public class HardcodedCredentialsRule extends AbstractCustomRule {
private static final PropertyDescriptor<List<String>> SENSITIVE_KEYWORDS =
PropertyFactory.stringListProperty("sensitiveKeywords")
.desc("Keywords that indicate sensitive data")
.defaultValues(Arrays.asList(
"password", "pwd", "pass", "secret", "key", "token", 
"credential", "apikey", "api_key", "privatekey", "private_key"
))
.build();
private static final PropertyDescriptor<List<String>> EXCLUDED_VALUES =
PropertyFactory.stringListProperty("excludedValues")
.desc("Values that are allowed (e.g., empty strings, placeholders)")
.defaultValues(Arrays.asList("", " ", "null", "NULL", "example", "test", "dummy"))
.build();
private static final PropertyDescriptor<Boolean> CHECK_STRINGS =
PropertyFactory.booleanProperty("checkStrings")
.desc("Check string literals for sensitive values")
.defaultValue(true)
.build();
private static final PropertyDescriptor<Boolean> CHECK_VARIABLE_NAMES =
PropertyFactory.booleanProperty("checkVariableNames")
.desc("Check variable names for sensitive keywords")
.defaultValue(true)
.build();
public HardcodedCredentialsRule() {
definePropertyDescriptor(SENSITIVE_KEYWORDS);
definePropertyDescriptor(EXCLUDED_VALUES);
definePropertyDescriptor(CHECK_STRINGS);
definePropertyDescriptor(CHECK_VARIABLE_NAMES);
}
@Override
public Object visit(ASTVariableDeclarator node, Object data) {
if (getProperty(CHECK_VARIABLE_NAMES)) {
checkVariableName(node, data);
}
return super.visit(node, data);
}
@Override
public Object visit(ASTLiteral node, Object data) {
if (getProperty(CHECK_STRINGS) && node.isStringLiteral()) {
checkStringLiteral(node, data);
}
return super.visit(node, data);
}
@Override
public Object visit(ASTAssignmentOperator node, Object data) {
checkAssignment(node, data);
return super.visit(node, data);
}
private void checkVariableName(ASTVariableDeclarator variable, Object data) {
String variableName = variable.getName().toLowerCase();
List<String> sensitiveKeywords = getProperty(SENSITIVE_KEYWORDS);
for (String keyword : sensitiveKeywords) {
if (variableName.contains(keyword)) {
// Check if this is initialized with a hardcoded value
ASTVariableInitializer initializer = variable.getFirstDescendantOfType(ASTVariableInitializer.class);
if (initializer != null && hasHardcodedValue(initializer)) {
addViolationWithMessage(data, variable,
"Potential hardcoded credential detected in variable '%s'. Use secure storage instead.",
variable.getName());
}
}
}
}
private void checkStringLiteral(ASTLiteral literal, Object data) {
String stringValue = literal.getImage();
if (stringValue != null && !isExcludedValue(stringValue)) {
// Check if this string looks like a credential
if (looksLikeCredential(stringValue)) {
// Check context - is this assigned to a sensitive variable?
ASTAssignmentOperator assignment = literal.getFirstParentOfType(ASTAssignmentOperator.class);
if (assignment != null) {
addViolationWithMessage(data, literal,
"Hardcoded string '%s' detected that may contain sensitive data",
stringValue.length() > 20 ? stringValue.substring(0, 20) + "..." : stringValue);
}
}
}
}
private void checkAssignment(ASTAssignmentOperator assignment, Object data) {
// Check if this is assigning a string literal to a sensitive variable
ASTLiteral literal = assignment.getFirstDescendantOfType(ASTLiteral.class);
if (literal != null && literal.isStringLiteral()) {
ASTVariableDeclarator variable = assignment.getFirstDescendantOfType(ASTVariableDeclarator.class);
if (variable != null && isSensitiveVariable(variable.getName())) {
String stringValue = literal.getImage();
if (!isExcludedValue(stringValue) && looksLikeCredential(stringValue)) {
addViolationWithMessage(data, assignment,
"Hardcoded credential assigned to sensitive variable '%s'",
variable.getName());
}
}
}
}
private boolean hasHardcodedValue(ASTVariableInitializer initializer) {
ASTLiteral literal = initializer.getFirstDescendantOfType(ASTLiteral.class);
return literal != null && literal.isStringLiteral() && 
!isExcludedValue(literal.getImage()) && looksLikeCredential(literal.getImage());
}
private boolean isSensitiveVariable(String variableName) {
String lowerName = variableName.toLowerCase();
List<String> sensitiveKeywords = getProperty(SENSITIVE_KEYWORDS);
return sensitiveKeywords.stream().anyMatch(lowerName::contains);
}
private boolean looksLikeCredential(String value) {
if (value == null || value.length() < 4) {
return false;
}
// Check for common credential patterns
return value.length() >= 8 || // Minimum password length
Pattern.compile("[0-9a-fA-F]{32,}").matcher(value).matches() || // MD5 hash-like
Pattern.compile("[0-9a-fA-F]{64,}").matcher(value).matches() || // SHA-256 hash-like
Pattern.compile("^[A-Za-z0-9+/]{20,}={0,2}$").matcher(value).matches(); // Base64-like
}
private boolean isExcludedValue(String value) {
List<String> excludedValues = getProperty(EXCLUDED_VALUES);
return excludedValues.contains(value) || 
value == null || 
value.trim().isEmpty();
}
@Override
public String getName() {
return "HardcodedCredentials";
}
@Override
public String getMessage() {
return "Potential hardcoded credential detected";
}
@Override
public String getDescription() {
return "Detects hardcoded passwords, API keys, and other secrets in the code";
}
}

4. Performance Rules

StringConcatenationInLoopRule.java - Detects inefficient string concatenation in loops

package com.yourcompany.pmd.rules.performance;
import com.yourcompany.pmd.rules.AbstractCustomRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
/**
* Rule that detects string concatenation in loops which should use StringBuilder.
*/
public class StringConcatenationInLoopRule extends AbstractCustomRule {
private static final PropertyDescriptor<Integer> MIN_ITERATIONS =
PropertyFactory.intProperty("minIterations")
.desc("Minimum number of iterations before reporting")
.defaultValue(3)
.build();
private static final PropertyDescriptor<Boolean> IGNORE_SINGLE_CONCAT =
PropertyFactory.booleanProperty("ignoreSingleConcat")
.desc("Ignore single concatenation in loop")
.defaultValue(true)
.build();
public StringConcatenationInLoopRule() {
definePropertyDescriptor(MIN_ITERATIONS);
definePropertyDescriptor(IGNORE_SINGLE_CONCAT);
}
@Override
public Object visit(ASTForStatement node, Object data) {
checkLoopForStringConcatenation(node, data);
return super.visit(node, data);
}
@Override
public Object visit(ASTWhileStatement node, Object data) {
checkLoopForStringConcatenation(node, data);
return super.visit(node, data);
}
@Override
public Object visit(ASTDoStatement node, Object data) {
checkLoopForStringConcatenation(node, data);
return super.visit(node, data);
}
@Override
public Object visit(ASTForEachStatement node, Object data) {
checkLoopForStringConcatenation(node, data);
return super.visit(node, data);
}
private void checkLoopForStringConcatenation(ASTLoopStatement loop, Object data) {
// Find all additive expressions in the loop body
List<ASTAdditiveExpression> additiveExprs = loop.findDescendantsOfType(ASTAdditiveExpression.class);
int stringConcatCount = 0;
for (ASTAdditiveExpression additiveExpr : additiveExprs) {
if (isStringConcatenation(additiveExpr)) {
stringConcatCount++;
// Check if this is in a loop that runs multiple times
if (isInLoopBody(additiveExpr, loop) && 
!isInExceptionContext(additiveExpr)) {
if (stringConcatCount >= getProperty(MIN_ITERATIONS) || 
!getProperty(IGNORE_SINGLE_CONCAT)) {
addViolationWithMessage(data, additiveExpr,
"String concatenation in loop detected. Use StringBuilder instead.");
}
}
}
}
}
private boolean isStringConcatenation(ASTAdditiveExpression expr) {
return "+".equals(expr.getImage()) && involvesStringType(expr);
}
private boolean involvesStringType(ASTAdditiveExpression expr) {
// Check if any operand is a string literal or has string type
List<ASTLiteral> literals = expr.findDescendantsOfType(ASTLiteral.class);
for (ASTLiteral literal : literals) {
if (literal.isStringLiteral()) {
return true;
}
}
// Check variable references (simplified check)
List<ASTPrimaryExpression> primaries = expr.findDescendantsOfType(ASTPrimaryExpression.class);
for (ASTPrimaryExpression primary : primaries) {
if (primary.getFirstDescendantOfType(ASTName.class) != null) {
// In a real implementation, you'd check the variable type here
return true; // Conservative approach
}
}
return false;
}
private boolean isInLoopBody(Node node, ASTLoopStatement loop) {
Node parent = node.getParent();
while (parent != null) {
if (parent == loop) {
return true;
}
if (parent instanceof ASTMethodDeclaration || parent instanceof ASTClassOrInterfaceDeclaration) {
break;
}
parent = parent.getParent();
}
return false;
}
private boolean isInExceptionContext(ASTAdditiveExpression expr) {
// Don't report if this is in exception message construction
Node parent = expr.getParent();
while (parent != null) {
if (parent instanceof ASTThrowStatement || 
parent instanceof ASTConstructorCall || 
hasAnyAnnotation(parent, "Exception", "Error")) {
return true;
}
parent = parent.getParent();
}
return false;
}
@Override
public String getName() {
return "StringConcatenationInLoop";
}
@Override
public String getMessage() {
return "Inefficient string concatenation in loop detected";
}
@Override
public String getDescription() {
return "Detects string concatenation in loops that should use StringBuilder for better performance";
}
}

5. Design Pattern Rules

SingletonPatternRule.java - Ensures proper singleton implementation

package com.yourcompany.pmd.rules.design;
import com.yourcompany.pmd.rules.AbstractCustomRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.List;
/**
* Rule that validates singleton pattern implementation.
*/
public class SingletonPatternRule extends AbstractCustomRule {
private static final PropertyDescriptor<Boolean> REQUIRE_PRIVATE_CONSTRUCTOR =
PropertyFactory.booleanProperty("requirePrivateConstructor")
.desc("Require private constructor")
.defaultValue(true)
.build();
private static final PropertyDescriptor<Boolean> REQUIRE_STATIC_INSTANCE =
PropertyFactory.booleanProperty("requireStaticInstance")
.desc("Require static instance field")
.defaultValue(true)
.build();
private static final PropertyDescriptor<Boolean> REQUIRE_GET_INSTANCE_METHOD =
PropertyFactory.booleanProperty("requireGetInstanceMethod")
.desc("Require getInstance() method")
.defaultValue(true)
.build();
public SingletonPatternRule() {
definePropertyDescriptor(REQUIRE_PRIVATE_CONSTRUCTOR);
definePropertyDescriptor(REQUIRE_STATIC_INSTANCE);
definePropertyDescriptor(REQUIRE_GET_INSTANCE_METHOD);
}
@Override
public Object visit(ASTClassOrInterfaceDeclaration node, Object data) {
if (isSingletonClass(node)) {
validateSingletonImplementation(node, data);
}
return super.visit(node, data);
}
private boolean isSingletonClass(ASTClassOrInterfaceDeclaration classDecl) {
String className = classDecl.getName();
// Check class name patterns that suggest singleton
if (className.endsWith("Singleton") || 
className.endsWith("Manager") || 
className.endsWith("Controller") || 
className.endsWith("Factory")) {
return true;
}
// Check for static instance field
List<ASTFieldDeclaration> fields = getFieldDeclarations(classDecl);
for (ASTFieldDeclaration field : fields) {
if (field.isStatic() && field.getType() != null && 
field.getType().getType() != null &&
field.getType().getType().equals(classDecl.getType())) {
return true;
}
}
return false;
}
private void validateSingletonImplementation(ASTClassOrInterfaceDeclaration classDecl, Object data) {
boolean hasPrivateConstructor = false;
boolean hasStaticInstance = false;
boolean hasGetInstanceMethod = false;
// Check constructors
List<ASTConstructorDeclaration> constructors = classDecl.findDescendantsOfType(ASTConstructorDeclaration.class);
for (ASTConstructorDeclaration constructor : constructors) {
if (constructor.isPrivate()) {
hasPrivateConstructor = true;
} else if (constructor.isPublic() || constructor.isProtected()) {
addViolationWithMessage(data, constructor,
"Singleton class '%s' should have private constructor", classDecl.getName());
}
}
// Check for static instance field
List<ASTFieldDeclaration> fields = getFieldDeclarations(classDecl);
for (ASTFieldDeclaration field : fields) {
if (field.isStatic() && field.getType() != null && 
field.getType().getType() != null &&
field.getType().getType().equals(classDecl.getType())) {
hasStaticInstance = true;
// Check if field is final
if (!field.isFinal()) {
addViolationWithMessage(data, field,
"Singleton instance field in '%s' should be final", classDecl.getName());
}
// Check if field is private
if (!field.isPrivate()) {
addViolationWithMessage(data, field,
"Singleton instance field in '%s' should be private", classDecl.getName());
}
}
}
// Check for getInstance method
List<ASTMethodDeclaration> methods = getMethodDeclarations(classDecl);
for (ASTMethodDeclaration method : methods) {
if (("getInstance".equals(method.getName()) || 
"get" + classDecl.getName().equals(method.getName())) &&
method.isStatic() && method.isPublic()) {
hasGetInstanceMethod = true;
// Check return type
ASTResultType resultType = method.getFirstDescendantOfType(ASTResultType.class);
if (resultType != null && resultType.getType() != null &&
!resultType.getType().equals(classDecl.getType())) {
addViolationWithMessage(data, method,
"getInstance() method in singleton '%s' should return the singleton type", 
classDecl.getName());
}
}
}
// Report missing requirements
if (getProperty(REQUIRE_PRIVATE_CONSTRUCTOR) && !hasPrivateConstructor && !constructors.isEmpty()) {
addViolationWithMessage(data, classDecl,
"Singleton class '%s' should have a private constructor", classDecl.getName());
}
if (getProperty(REQUIRE_STATIC_INSTANCE) && !hasStaticInstance) {
addViolationWithMessage(data, classDecl,
"Singleton class '%s' should have a static instance field", classDecl.getName());
}
if (getProperty(REQUIRE_GET_INSTANCE_METHOD) && !hasGetInstanceMethod) {
addViolationWithMessage(data, classDecl,
"Singleton class '%s' should have a getInstance() method", classDecl.getName());
}
}
@Override
public String getName() {
return "SingletonPattern";
}
@Override
public String getMessage() {
return "Singleton pattern implementation issue";
}
@Override
public String getDescription() {
return "Validates proper implementation of the singleton pattern";
}
}

6. Custom Ruleset Configuration

custom-rules.xml - PMD ruleset configuration

<?xml version="1.0"?>
<ruleset name="Custom Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 
https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
Custom PMD rules for our project coding standards and best practices.
</description>
<!-- Include standard PMD rules -->
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml">
<exclude name="AtLeastOneConstructor"/>
<exclude name="LocalVariableCouldBeFinal"/>
<exclude name="MethodArgumentCouldBeFinal"/>
</rule>
<rule ref="category/java/design.xml">
<exclude name="LawOfDemeter"/>
<exclude name="DataClass"/>
</rule>
<!-- Custom Naming Rules -->
<rule name="CustomMethodNaming"
message="Method naming convention violation"
class="com.yourcompany.pmd.rules.naming.MethodNamingRule">
<description>Enforces consistent method naming conventions</description>
<priority>3</priority>
<properties>
<property name="allowUnderscores" value="false"/>
<property name="testMethodPattern" value="^test[A-Z][a-zA-Z0-9]*$|^[a-z][a-zA-Z0-9]*Test$"/>
<property name="privateMethodPattern" value="^[a-z][a-zA-Z0-9]*$"/>
</properties>
<example>
<![CDATA[
// Valid
public void calculateTotalPrice() {}
private void validateInput() {}
public void testUserCreation() {}
// Invalid
public void Calculate_Total_Price() {} // Underscores and uppercase start
private void ValidateInput() {} // Uppercase start for private method
]]>
</example>
</rule>
<rule name="CustomConstantNaming"
message="Constant naming convention violation"
class="com.yourcompany.pmd.rules.naming.ConstantNamingRule">
<description>Enforces UPPER_CASE naming convention for constant fields</description>
<priority>3</priority>
<properties>
<property name="constantPattern" value="^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$"/>
<property name="checkStaticFinal" value="true"/>
</properties>
<example>
<![CDATA[
// Valid
public static final int MAX_RETRY_COUNT = 3;
private static final String DATABASE_URL = "jdbc:mysql://localhost:3306/mydb";
// Invalid
public static final int maxRetryCount = 3; // Lowercase
private static final String databaseUrl = "jdbc:..."; // Lowercase
private static final String DATABASEURL = "jdbc:..."; // No underscores
]]>
</example>
</rule>
<!-- Security Rules -->
<rule name="HardcodedCredentials"
message="Potential hardcoded credential detected"
class="com.yourcompany.pmd.rules.security.HardcodedCredentialsRule">
<description>Detects hardcoded passwords, API keys, and other secrets</description>
<priority>1</priority> <!-- High priority for security issues -->
<properties>
<property name="sensitiveKeywords" type="List" value="password,pwd,pass,secret,key,token,credential,apikey,api_key"/>
<property name="excludedValues" type="List" value=", ,null,NULL,example,test,dummy"/>
<property name="checkStrings" value="true"/>
<property name="checkVariableNames" value="true"/>
</properties>
<example>
<![CDATA[
// Violations
String password = "mySecretPassword123"; // Hardcoded password
String apiKey = "sk_1234567890abcdef"; // Hardcoded API key
// Correct approach
String password = System.getenv("DB_PASSWORD"); // From environment
String apiKey = config.getApiKey(); // From configuration
]]>
</example>
</rule>
<!-- Performance Rules -->
<rule name="StringConcatenationInLoop"
message="Inefficient string concatenation in loop detected"
class="com.yourcompany.pmd.rules.performance.StringConcatenationInLoopRule">
<description>Detects string concatenation in loops that should use StringBuilder</description>
<priority>3</priority>
<properties>
<property name="minIterations" value="3"/>
<property name="ignoreSingleConcat" value="true"/>
</properties>
<example>
<![CDATA[
// Violation
String result = "";
for (int i = 0; i < 100; i++) {
result += "Item " + i + ", "; // Inefficient concatenation
}
// Correct
StringBuilder result = new StringBuilder();
for (int i = 0; i < 100; i++) {
result.append("Item ").append(i).append(", ");
}
String finalResult = result.toString();
]]>
</example>
</rule>
<!-- Design Pattern Rules -->
<rule name="SingletonPattern"
message="Singleton pattern implementation issue"
class="com.yourcompany.pmd.rules.design.SingletonPatternRule">
<description>Validates proper implementation of the singleton pattern</description>
<priority>3</priority>
<properties>
<property name="requirePrivateConstructor" value="true"/>
<property name="requireStaticInstance" value="true"/>
<property name="requireGetInstanceMethod" value="true"/>
</properties>
<example>
<![CDATA[
// Valid singleton
public class DatabaseConnection {
private static final DatabaseConnection INSTANCE = new DatabaseConnection();
private DatabaseConnection() {
// private constructor
}
public static DatabaseConnection getInstance() {
return INSTANCE;
}
}
// Invalid - public constructor
public class InvalidSingleton {
public static final InvalidSingleton INSTANCE = new InvalidSingleton();
public InvalidSingleton() { // Should be private
}
}
]]>
</example>
</rule>
<!-- Exclude generated code and test sources -->
<exclude-pattern>.*/generated-sources/.*</exclude-pattern>
<exclude-pattern>.*/target/.*</exclude-pattern>
<exclude-pattern>.*/test/.*</exclude-pattern>
</ruleset>

7. Testing Custom Rules

MethodNamingRuleTest.java - Unit tests for custom rules

package com.yourcompany.pmd.rules.test;
import com.yourcompany.pmd.rules.naming.MethodNamingRule;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ParserTstUtil;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MethodNamingRuleTest {
private MethodNamingRule rule;
private RuleContext context;
@BeforeEach
void setUp() {
rule = new MethodNamingRule();
context = new RuleContext();
}
@Test
void shouldDetectInvalidTestMethodName() throws Exception {
String code = """
import org.junit.Test;
public class UserServiceTest {
@Test
public void testuserCreation() { // Should be testUserCreation
// test method
}
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).hasSize(1);
assertThat(context.getReport().violations().get(0).getDescription())
.contains("Test method name 'testuserCreation' does not match pattern");
}
@Test
void shouldAllowValidTestMethodName() throws Exception {
String code = """
import org.junit.Test;
public class UserServiceTest {
@Test
public void testUserCreation() {
// valid test method
}
@Test 
public void userCreationTest() {
// also valid
}
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).isEmpty();
}
@Test
void shouldDetectInvalidPrivateMethodName() throws Exception {
String code = """
public class UserService {
private void ValidateUser() { // Should be validateUser
// private method
}
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).hasSize(1);
assertThat(context.getReport().violations().get(0).getDescription())
.contains("Private method name 'ValidateUser' should start with lowercase letter");
}
@Test
void shouldDetectUnderscoresInMethodName() throws Exception {
String code = """
public class OrderProcessor {
public void process_order() { // Underscores not allowed
// method implementation
}
}
""";
rule.setProperty(MethodNamingRule.ALLOW_UNDERSCORES, false);
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).hasSize(1);
assertThat(context.getReport().violations().get(0).getDescription())
.contains("Method name 'process_order' should not contain underscores");
}
}
**HardcodedCredentialsRuleTest.java**

java
package com.yourcompany.pmd.rules.test;

import com.yourcompany.pmd.rules.security.HardcodedCredentialsRule;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ParserTstUtil;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.StringReader;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class HardcodedCredentialsRuleTest {

private HardcodedCredentialsRule rule;
private RuleContext context;
@BeforeEach
void setUp() {
rule = new HardcodedCredentialsRule();
context = new RuleContext();
}
@Test
void shouldDetectHardcodedPassword() throws Exception {
String code = """
public class DatabaseConfig {
private String password = "mySecretPassword123";
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).hasSize(1);
assertThat(context.getReport().violations().get(0).getDescription())
.contains("Potential hardcoded credential detected");
}
@Test
void shouldDetectHardcodedApiKey() throws Exception {
String code = """
public class ApiClient {
private static final String API_KEY = "sk_1234567890abcdef";
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).hasSize(1);
assertThat(context.getReport().violations().get(0).getDescription())
.contains("Potential hardcoded credential detected");
}
@Test
void shouldIgnoreExampleValues() throws Exception {
String code = """
public class Examples {
private String examplePassword = "example";
private String testKey = "test";
private String dummyToken = "dummy";
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).isEmpty();
}
@Test
void shouldDetectSensitiveVariableNames() throws Exception {
String code = """
public class SecurityConfig {
private String secret = "actualSecretValue";
private String api_key = "realApiKey";
}
""";
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
assertThat(context.getReport().violations()).hasSize(2);
}

}

### 8. Rule Development Utilities
**RuleDevelopmentUtils.java** - Utilities for developing and testing rules

java
package com.yourcompany.pmd.rules.util;

import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ParserTstUtil;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;

import java.io.StringReader;
import java.util.List;

public class RuleDevelopmentUtils {

/**
* Test a rule against a code snippet and return the violations.
*/
public static List<net.sourceforge.pmd.RuleViolation> testRule(
AbstractJavaRule rule, String code) throws Exception {
RuleContext context = new RuleContext();
List<ASTCompilationUnit> nodes = ParserTstUtil.parseJava(new StringReader(code));
rule.apply(nodes, context);
return context.getReport().violations();
}
/**
* Create a complete Java class string for testing.
*/
public static String createClass(String className, String... methods) {
StringBuilder sb = new StringBuilder();
sb.append("public class ").append(className).append(" {\n");
for (String method : methods) {
sb.append("    ").append(method).append("\n");
}
sb.append("}\n");
return sb.toString();
}
/**
* Create a method string for testing.
*/
public static String createMethod(String modifiers, String returnType, 
String methodName, String body) {
return String.format("%s %s %s() { %s }", 
modifiers, returnType, methodName, body);
}

}

## Usage Examples
### Maven Configuration
**pom.xml** (additional configuration)

xml
org.apache.maven.plugins maven-pmd-plugin 3.20.0 custom-rules.xml true 3 true /generated-sources/ /test/ check cpd-check

### Running PMD with Custom Rules

bash

Run PMD with custom rules

mvn pmd:check

Generate PMD report

mvn pmd:pmd

Run only custom rules

mvn pmd:check -Dpmd.rulesets=custom-rules.xml
```

IDE Integration

Most IDEs can be configured to use custom PMD rules:

  1. Eclipse: Install PMD plugin and add custom ruleset
  2. IntelliJ: Install PMD plugin and configure custom rules
  3. VS Code: Use Java extension with PMD support

Best Practices for Custom Rules

  1. Start Simple: Begin with basic rules and gradually add complexity
  2. Test Thoroughly: Create comprehensive tests for each rule
  3. Use Properties: Make rules configurable through properties
  4. Provide Examples: Include good and bad code examples in documentation
  5. Consider Performance: Ensure rules don't significantly impact analysis time
  6. Handle Edge Cases: Account for various coding patterns and styles
  7. Follow PMD Conventions: Use standard PMD patterns and utilities
  8. Document Clearly: Provide clear descriptions and messages

This comprehensive implementation provides a solid foundation for creating custom PMD rules that enforce your project's specific coding standards, security requirements, and design patterns.

Secure Java Dependency Management, Vulnerability Scanning & Software Supply Chain Protection (SBOM, SCA, CI Security & License Compliance)

https://macronepal.com/blog/github-code-scanning-in-java-complete-guide/
Explains GitHub Code Scanning for Java using tools like CodeQL to automatically analyze source code and detect security vulnerabilities directly inside CI/CD pipelines before deployment.

https://macronepal.com/blog/license-compliance-in-java-comprehensive-guide/
Explains software license compliance in Java projects, ensuring dependencies follow legal requirements (MIT, Apache, GPL, etc.) and preventing license violations in enterprise software.

https://macronepal.com/blog/container-security-for-java-uncovering-vulnerabilities-with-grype/
Explains using Grype to scan Java container images and filesystems for known CVEs in OS packages and application dependencies to improve container security.

https://macronepal.com/blog/syft-sbom-generation-in-java-comprehensive-software-bill-of-materials-for-jvm-applications/
Explains using Syft to generate SBOMs (Software Bill of Materials) for Java applications, listing all dependencies, libraries, and components for supply chain transparency.

https://macronepal.com/blog/comprehensive-dependency-analysis-generating-and-scanning-sboms-with-trivy-for-java/
Explains using Trivy to generate SBOMs and scan Java dependencies and container images for vulnerabilities, integrating security checks into CI/CD pipelines.

https://macronepal.com/blog/dependabot-for-java-in-java/
Explains GitHub Dependabot for Java projects, which automatically detects vulnerable dependencies and creates pull requests to update them securely.

https://macronepal.com/blog/parasoft-jtest-in-java-comprehensive-guide-to-code-analysis-and-testing/
Explains Parasoft Jtest, a static analysis and testing tool for Java that helps detect bugs, security issues, and code quality problems early in development.

https://macronepal.com/blog/snyk-open-source-in-java-comprehensive-dependency-vulnerability-management-2/
Explains Snyk Open Source for Java, which continuously scans dependencies for vulnerabilities and provides automated fix suggestions and monitoring.

https://macronepal.com/blog/owasp-dependency-check-in-java-complete-vulnerability-scanning-guide/
Explains OWASP Dependency-Check, which scans Java dependencies against the National Vulnerability Database (NVD) to detect known security vulnerabilities.

https://macronepal.com/blog/securing-your-dependencies-a-java-developers-guide-to-whitesource-mend-bolt/
Explains Mend (WhiteSource) Bolt for Java, a dependency management and SCA tool that provides vulnerability detection, license compliance, and security policy enforcement in enterprise environments.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper