PMD is a powerful static code analysis tool that helps identify coding issues, bugs, and code smells. Creating custom PMD rules allows you to enforce team-specific coding standards and practices.
Setup and Dependencies
Maven Dependencies
<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>
<!-- For testing rules -->
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-test</artifactId>
<version>${pmd.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>${maven-pmd-plugin.version}</version>
</plugin>
</plugins>
</build>
Project Structure
custom-pmd-rules/ ├── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── example/ │ │ └── pmd/ │ │ ├── rules/ │ │ └── category/ │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── pmd/ │ └── rules/ ├── resources/ │ └── rulesets/ └── pom.xml
Custom Rule Implementation
1. Base Rule Structure
package com.example.pmd.category;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.*;
public abstract class CustomJavaRule extends AbstractJavaRule {
protected static final PropertyDescriptor<Integer> MAX_LENGTH_PROPERTY =
PropertyFactory.intProperty("maxLength")
.desc("Maximum allowed length")
.defaultValue(30)
.build();
protected static final PropertyDescriptor<Boolean> CHECK_PUBLIC_METHODS_PROPERTY =
PropertyFactory.booleanProperty("checkPublicMethods")
.desc("Check public methods only")
.defaultValue(true)
.build();
public CustomJavaRule() {
definePropertyDescriptor(MAX_LENGTH_PROPERTY);
definePropertyDescriptor(CHECK_PUBLIC_METHODS_PROPERTY);
}
protected boolean isPublicMethod(ASTMethodDeclaration method) {
return method.getModifiers().isPublic();
}
protected boolean isTestClass(ASTClassOrInterfaceDeclaration classDecl) {
String className = classDecl.getSimpleName();
return className.endsWith("Test") || className.endsWith("IT");
}
protected void addViolationWithMessage(Object data, JavaNode node, String message) {
RuleContext context = (RuleContext) data;
context.getReport().addViolation(createViolation(context, node, message));
}
}
2. Custom Rule Category
package com.example.pmd.category;
public class CustomRulesCategory {
public static final String NAME = "Custom Rules";
private CustomRulesCategory() {
// Utility class
}
}
Custom Rule Examples
1. Method Naming Convention Rule
package com.example.pmd.rules;
import com.example.pmd.category.CustomJavaRule;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.regex.Pattern;
public class MethodNamingConventionRule extends CustomJavaRule {
private static final PropertyDescriptor<Pattern> METHOD_NAME_PATTERN_PROPERTY =
PropertyFactory.regexProperty("methodNamePattern")
.desc("Regex pattern for method names")
.defaultValue(Pattern.compile("^[a-z][a-zA-Z0-9]*$"))
.build();
private static final PropertyDescriptor<Boolean> ALLOW_UNDERSCORES_PROPERTY =
PropertyFactory.booleanProperty("allowUnderscores")
.desc("Allow underscores in method names")
.defaultValue(false)
.build();
public MethodNamingConventionRule() {
definePropertyDescriptor(METHOD_NAME_PATTERN_PROPERTY);
definePropertyDescriptor(ALLOW_UNDERSCORES_PROPERTY);
}
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
// Skip test methods
if (isTestMethod(node)) {
return super.visit(node, data);
}
// Check if we should only validate public methods
boolean checkPublicOnly = getProperty(CHECK_PUBLIC_METHODS_PROPERTY);
if (checkPublicOnly && !isPublicMethod(node)) {
return super.visit(node, data);
}
ASTMethodDeclarator declarator = node.getFirstDescendantOfType(ASTMethodDeclarator.class);
if (declarator != null) {
String methodName = declarator.getName();
// Check naming convention
Pattern pattern = getProperty(METHOD_NAME_PATTERN_PROPERTY);
boolean allowUnderscores = getProperty(ALLOW_UNDERSCORES_PROPERTY);
if (!pattern.matcher(methodName).matches()) {
addViolation(data, declarator,
"Method name '" + methodName + "' does not match naming convention: " + pattern.pattern());
}
// Additional underscore check
if (!allowUnderscores && methodName.contains("_")) {
addViolation(data, declarator,
"Method name '" + methodName + "' should not contain underscores");
}
// Check for getter/setter naming
checkGetterSetterNaming(node, declarator, data);
}
return super.visit(node, data);
}
private boolean isTestMethod(ASTMethodDeclaration node) {
// Check for @Test annotation
if (node.getAnnotations() != null) {
for (ASTAnnotation annotation : node.findDescendantsOfType(ASTAnnotation.class)) {
if (annotation.getAnnotationName().equals("Test")) {
return true;
}
}
}
// Check method name pattern for tests
ASTMethodDeclarator declarator = node.getFirstDescendantOfType(ASTMethodDeclarator.class);
if (declarator != null) {
String methodName = declarator.getName();
return methodName.startsWith("test") || methodName.endsWith("Test");
}
return false;
}
private void checkGetterSetterNaming(ASTMethodDeclaration node,
ASTMethodDeclarator declarator,
Object data) {
String methodName = declarator.getName();
if (methodName.startsWith("get") || methodName.startsWith("set")) {
// Should have exactly 0 parameters for getters, 1 for setters
int parameterCount = declarator.getParameterCount();
if (methodName.startsWith("get") && parameterCount > 0) {
addViolation(data, declarator,
"Getter method '" + methodName + "' should not have parameters");
}
if (methodName.startsWith("set") && parameterCount != 1) {
addViolation(data, declarator,
"Setter method '" + methodName + "' should have exactly one parameter");
}
// Check that getter/setter names follow JavaBean convention
if (methodName.length() > 3) {
String propertyName = methodName.substring(3);
if (propertyName.isEmpty() || !Character.isUpperCase(propertyName.charAt(0))) {
addViolation(data, declarator,
"Getter/Setter method '" + methodName + "' should follow JavaBean naming convention");
}
}
}
// Check boolean getters (should start with 'is' or 'get')
if (methodName.startsWith("is") && node.getResultType().isBooleanType()) {
if (declarator.getParameterCount() > 0) {
addViolation(data, declarator,
"Boolean getter method '" + methodName + "' should not have parameters");
}
}
}
}
2. Logger Declaration Rule
package com.example.pmd.rules;
import com.example.pmd.category.CustomJavaRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.List;
public class LoggerDeclarationRule extends CustomJavaRule {
private static final PropertyDescriptor<String> LOGGER_TYPE_PROPERTY =
PropertyFactory.stringProperty("loggerType")
.desc("Expected logger type (SLF4J, Log4j, etc.)")
.defaultValue("org.slf4j.Logger")
.build();
private static final PropertyDescriptor<Boolean> REQUIRE_STATIC_FINAL_PROPERTY =
PropertyFactory.booleanProperty("requireStaticFinal")
.desc("Require logger to be static final")
.defaultValue(true)
.build();
private static final PropertyDescriptor<String> LOGGER_NAME_PROPERTY =
PropertyFactory.stringProperty("loggerName")
.desc("Expected logger variable name")
.defaultValue("logger")
.build();
public LoggerDeclarationRule() {
definePropertyDescriptor(LOGGER_TYPE_PROPERTY);
definePropertyDescriptor(REQUIRE_STATIC_FINAL_PROPERTY);
definePropertyDescriptor(LOGGER_NAME_PROPERTY);
}
@Override
public Object visit(ASTClassOrInterfaceDeclaration node, Object data) {
// Skip test classes
if (isTestClass(node)) {
return super.visit(node, data);
}
List<ASTFieldDeclaration> fieldDeclarations = node.findDescendantsOfType(ASTFieldDeclaration.class);
boolean hasLogger = false;
for (ASTFieldDeclaration field : fieldDeclarations) {
if (isLoggerField(field)) {
hasLogger = true;
validateLoggerField(field, data);
}
}
// Check if class should have a logger
if (!hasLogger && shouldHaveLogger(node)) {
addViolation(data, node,
"Class should declare a logger field of type: " + getProperty(LOGGER_TYPE_PROPERTY));
}
return super.visit(node, data);
}
private boolean isLoggerField(ASTFieldDeclaration field) {
ASTType type = field.getFirstDescendantOfType(ASTType.class);
if (type != null) {
String typeName = type.getTypeImage();
String expectedType = getProperty(LOGGER_TYPE_PROPERTY);
return typeName != null && typeName.equals(expectedType);
}
return false;
}
private void validateLoggerField(ASTFieldDeclaration field, Object data) {
// Check modifiers
if (getProperty(REQUIRE_STATIC_FINAL_PROPERTY)) {
ASTModifierList modifiers = field.getFirstDescendantOfType(ASTModifierList.class);
if (modifiers != null) {
if (!modifiers.isStatic()) {
addViolation(data, field, "Logger field should be static");
}
if (!modifiers.isFinal()) {
addViolation(data, field, "Logger field should be final");
}
}
}
// Check field name
ASTVariableDeclaratorId declarator = field.getFirstDescendantOfType(ASTVariableDeclaratorId.class);
if (declarator != null) {
String expectedName = getProperty(LOGGER_NAME_PROPERTY);
if (!declarator.getName().equals(expectedName)) {
addViolation(data, field,
"Logger field should be named '" + expectedName + "'");
}
}
// Check initialization
ASTVariableInitializer initializer = field.getFirstDescendantOfType(ASTVariableInitializer.class);
if (initializer == null) {
addViolation(data, field, "Logger field should be initialized");
}
}
private boolean shouldHaveLogger(ASTClassOrInterfaceDeclaration classDecl) {
// Don't require logger in interfaces, annotations, or enums
if (classDecl.isInterface() || classDecl.isAnnotation() || classDecl.isEnum()) {
return false;
}
// Don't require logger in simple data classes (only fields, no methods)
List<ASTMethodDeclaration> methods = classDecl.findDescendantsOfType(ASTMethodDeclaration.class);
boolean hasBusinessMethods = methods.stream()
.anyMatch(method -> !method.getMethodName().startsWith("get") &&
!method.getMethodName().startsWith("set") &&
!method.getMethodName().equals("equals") &&
!method.getMethodName().equals("hashCode") &&
!method.getMethodName().equals("toString"));
return hasBusinessMethods;
}
}
3. Exception Handling Rule
package com.example.pmd.rules;
import com.example.pmd.category.CustomJavaRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import java.util.List;
public class ExceptionHandlingRule extends CustomJavaRule {
private static final PropertyDescriptor<Boolean> REQUIRE_CAUSE_PROPERTY =
PropertyFactory.booleanProperty("requireCause")
.desc("Require exception cause to be passed when wrapping exceptions")
.defaultValue(true)
.build();
private static final PropertyDescriptor<Boolean> LOG_AND_THROW_PROPERTY =
PropertyFactory.booleanProperty("forbidLogAndThrow")
.desc("Forbid logging and re-throwing the same exception")
.defaultValue(true)
.build();
private static final PropertyDescriptor<List<String>> IGNORED_EXCEPTIONS_PROPERTY =
PropertyFactory.stringListProperty("ignoredExceptions")
.desc("List of exception types to ignore")
.defaultValues("InterruptedException", "NumberFormatException")
.build();
public ExceptionHandlingRule() {
definePropertyDescriptor(REQUIRE_CAUSE_PROPERTY);
definePropertyDescriptor(LOG_AND_THROW_PROPERTY);
definePropertyDescriptor(IGNORED_EXCEPTIONS_PROPERTY);
}
@Override
public Object visit(ASTThrowStatement node, Object data) {
ASTPrimaryExpression expression = node.getFirstDescendantOfType(ASTPrimaryExpression.class);
if (expression != null) {
checkExceptionConstruction(expression, data);
}
return super.visit(node, data);
}
@Override
public Object visit(ASTCatchStatement node, Object data) {
checkCatchBlock(node, data);
return super.visit(node, data);
}
private void checkExceptionConstruction(ASTPrimaryExpression expression, Object data) {
// Check for new Exception() constructions
ASTAllocationExpression allocation = expression.getFirstDescendantOfType(ASTAllocationExpression.class);
if (allocation != null) {
ASTClassOrInterfaceType exceptionType = allocation.getFirstDescendantOfType(ASTClassOrInterfaceType.class);
if (exceptionType != null && isExceptionType(exceptionType.getImage())) {
checkExceptionConstructorArguments(allocation, data);
}
}
}
private void checkExceptionConstructorArguments(ASTAllocationExpression allocation, Object data) {
if (!getProperty(REQUIRE_CAUSE_PROPERTY)) {
return;
}
ASTArguments arguments = allocation.getFirstDescendantOfType(ASTArguments.class);
if (arguments != null) {
int argCount = arguments.getArgumentCount();
// Check if this is wrapping an existing exception
boolean hasSurroundingTry = hasSurroundingTryBlock(allocation);
if (hasSurroundingTry && argCount == 1) {
// Only message provided, but we're in a catch block - should include cause
addViolation(data, allocation,
"When wrapping exceptions, include the original exception as cause");
}
}
}
private void checkCatchBlock(ASTCatchStatement catchStmt, Object data) {
if (!getProperty(LOG_AND_THROW_PROPERTY)) {
return;
}
ASTBlock block = catchStmt.getFirstDescendantOfType(ASTBlock.class);
if (block != null) {
List<ASTStatement> statements = block.findDescendantsOfType(ASTStatement.class);
boolean hasLog = false;
boolean hasThrow = false;
for (ASTStatement stmt : statements) {
if (isLoggingStatement(stmt)) {
hasLog = true;
}
if (stmt.hasDescendantOfType(ASTThrowStatement.class)) {
hasThrow = true;
}
}
if (hasLog && hasThrow) {
addViolation(data, catchStmt,
"Avoid logging and re-throwing the same exception - it creates duplicate log entries");
}
}
// Check for empty catch blocks
checkEmptyCatchBlock(catchStmt, data);
}
private void checkEmptyCatchBlock(ASTCatchStatement catchStmt, Object data) {
ASTBlock block = catchStmt.getFirstDescendantOfType(ASTBlock.class);
if (block != null) {
List<ASTStatement> statements = block.findDescendantsOfType(ASTStatement.class);
if (statements.isEmpty() ||
(statements.size() == 1 && isCommentOnlyBlock(statements.get(0)))) {
ASTFormalParameter param = catchStmt.getFirstDescendantOfType(ASTFormalParameter.class);
if (param != null) {
String exceptionType = param.getTypeNode().getImage();
if (!isIgnoredException(exceptionType)) {
addViolation(data, catchStmt,
"Empty catch block for exception: " + exceptionType +
" - at least log the exception");
}
}
}
}
}
private boolean isExceptionType(String typeName) {
return typeName.endsWith("Exception") ||
typeName.endsWith("Error") ||
"Throwable".equals(typeName);
}
private boolean hasSurroundingTryBlock(JavaNode node) {
return node.getFirstParentOfType(ASTTryStatement.class) != null;
}
private boolean isLoggingStatement(ASTStatement stmt) {
// Simple check for logger calls
String statementText = stmt.getImage();
return statementText != null &&
(statementText.contains("logger.") ||
statementText.contains("log.") ||
statementText.contains("LOG."));
}
private boolean isCommentOnlyBlock(ASTStatement stmt) {
return stmt.findDescendantsOfType(ASTEmptyStatement.class).size() > 0;
}
private boolean isIgnoredException(String exceptionType) {
return getProperty(IGNORED_EXCEPTIONS_PROPERTY).contains(exceptionType);
}
}
4. Security Rule - Hardcoded Credentials
package com.example.pmd.rules;
import com.example.pmd.category.CustomJavaRule;
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;
public class HardcodedCredentialsRule extends CustomJavaRule {
private static final PropertyDescriptor<List<String>> SENSITIVE_KEYS_PROPERTY =
PropertyFactory.stringListProperty("sensitiveKeys")
.desc("List of sensitive key patterns")
.defaultValues("password", "secret", "key", "token", "credential", "auth")
.build();
private static final PropertyDescriptor<Pattern> PASSWORD_PATTERN_PROPERTY =
PropertyFactory.regexProperty("passwordPattern")
.desc("Pattern to identify potential passwords")
.defaultValue(Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"))
.build();
public HardcodedCredentialsRule() {
definePropertyDescriptor(SENSITIVE_KEYS_PROPERTY);
definePropertyDescriptor(PASSWORD_PATTERN_PROPERTY);
}
@Override
public Object visit(ASTVariableDeclarator node, Object data) {
checkVariableDeclaration(node, data);
return super.visit(node, data);
}
@Override
public Object visit(ASTAssignmentOperator node, Object data) {
checkAssignment(node, data);
return super.visit(node, data);
}
@Override
public Object visit(ASTLiteral node, Object data) {
checkLiteral(node, data);
return super.visit(node, data);
}
private void checkVariableDeclaration(ASTVariableDeclarator node, Object data) {
String variableName = node.getName();
ASTVariableInitializer initializer = node.getFirstDescendantOfType(ASTVariableInitializer.class);
if (initializer != null && isSensitiveVariable(variableName)) {
checkForHardcodedValue(initializer, variableName, data);
}
}
private void checkAssignment(ASTAssignmentOperator node, Object data) {
ASTPrimaryExpression lhs = node.getFirstDescendantOfType(ASTPrimaryExpression.class);
if (lhs != null) {
ASTPrimarySuffix suffix = lhs.getFirstDescendantOfType(ASTPrimarySuffix.class);
if (suffix != null && isSensitiveVariable(suffix.getImage())) {
addViolation(data, node,
"Potential hardcoded credential assignment to: " + suffix.getImage());
}
}
}
private void checkLiteral(ASTLiteral node, Object data) {
if (node.isStringLiteral()) {
String value = node.getImage();
if (value != null && looksLikePassword(value)) {
// Check if this literal is assigned to a sensitive variable
ASTVariableDeclarator declarator = node.getFirstParentOfType(ASTVariableDeclarator.class);
if (declarator != null && isSensitiveVariable(declarator.getName())) {
addViolation(data, node,
"Potential hardcoded password detected: " + value);
}
}
}
}
private void checkForHardcodedValue(ASTVariableInitializer initializer,
String variableName, Object data) {
ASTLiteral literal = initializer.getFirstDescendantOfType(ASTLiteral.class);
if (literal != null && literal.isStringLiteral()) {
String value = literal.getImage();
if (value != null && value.length() > 3) { // Avoid very short values
addViolation(data, initializer,
"Hardcoded credential detected in variable: " + variableName);
}
}
}
private boolean isSensitiveVariable(String variableName) {
if (variableName == null) return false;
String lowerName = variableName.toLowerCase();
return getProperty(SENSITIVE_KEYS_PROPERTY).stream()
.anyMatch(key -> lowerName.contains(key.toLowerCase()));
}
private boolean looksLikePassword(String value) {
if (value == null || value.length() < 8) return false;
Pattern pattern = getProperty(PASSWORD_PATTERN_PROPERTY);
return pattern.matcher(value).matches();
}
}
5. Performance Rule - String Concatenation in Loops
package com.example.pmd.rules;
import com.example.pmd.category.CustomJavaRule;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
public class StringConcatenationInLoopRule extends CustomJavaRule {
private static final PropertyDescriptor<Integer> MIN_ITERATIONS_PROPERTY =
PropertyFactory.intProperty("minIterations")
.desc("Minimum number of iterations to consider it a loop")
.defaultValue(3)
.build();
public StringConcatenationInLoopRule() {
definePropertyDescriptor(MIN_ITERATIONS_PROPERTY);
}
@Override
public Object visit(ASTWhileStatement node, Object data) {
checkLoopForStringConcatenation(node, data);
return super.visit(node, data);
}
@Override
public Object visit(ASTForStatement 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(ASTLoopingStatement loop, Object data) {
List<ASTAdditiveExpression> additiveExprs = loop.findDescendantsOfType(ASTAdditiveExpression.class);
for (ASTAdditiveExpression expr : additiveExprs) {
if (isStringConcatenation(expr)) {
// Check if this is inside the loop body (not in the loop control)
if (isInLoopBody(expr, loop)) {
addViolation(data, expr,
"String concatenation in loop - use StringBuilder instead");
}
}
}
}
private boolean isStringConcatenation(ASTAdditiveExpression expr) {
return "+".equals(expr.getImage());
}
private boolean isInLoopBody(ASTAdditiveExpression expr, ASTLoopingStatement loop) {
// Check if the expression is in the loop body (not in initialization or condition)
ASTStatement loopBody = getLoopBody(loop);
return loopBody != null && loopBody.hasDescendantOfType(ASTAdditiveExpression.class);
}
private ASTStatement getLoopBody(ASTLoopingStatement loop) {
if (loop instanceof ASTWhileStatement) {
return ((ASTWhileStatement) loop).getStatement();
} else if (loop instanceof ASTForStatement) {
return ((ASTForStatement) loop).getStatement();
} else if (loop instanceof ASTDoStatement) {
return ((ASTDoStatement) loop).getStatement();
} else if (loop instanceof ASTForEachStatement) {
return ((ASTForEachStatement) loop).getStatement();
}
return null;
}
}
Ruleset Configuration
1. Custom Ruleset XML
<?xml version="1.0"?>
<ruleset name="Custom Java 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</description>
<!-- Import standard PMD rules -->
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml"/>
<rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/performance.xml"/>
<!-- Custom Rules -->
<rule name="MethodNamingConvention"
language="java"
message="Method name violation"
class="com.example.pmd.rules.MethodNamingConventionRule">
<description>Enforces method naming conventions</description>
<priority>3</priority>
<properties>
<property name="methodNamePattern" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="allowUnderscores" value="false"/>
<property name="checkPublicMethods" value="true"/>
</properties>
<example>
<![CDATA[
public class MyClass {
public void BadMethodName() { } // Violation
public void goodMethodName() { } // OK
}
]]>
</example>
</rule>
<rule name="LoggerDeclaration"
language="java"
message="Logger declaration violation"
class="com.example.pmd.rules.LoggerDeclarationRule">
<description>Enforces proper logger declaration</description>
<priority>2</priority>
<properties>
<property name="loggerType" value="org.slf4j.Logger"/>
<property name="requireStaticFinal" value="true"/>
<property name="loggerName" value="logger"/>
</properties>
<example>
<![CDATA[
public class MyClass {
private Logger log; // Violation - should be static final and named 'logger'
}
]]>
</example>
</rule>
<rule name="ExceptionHandling"
language="java"
message="Exception handling violation"
class="com.example.pmd.rules.ExceptionHandlingRule">
<description>Enforces proper exception handling practices</description>
<priority>2</priority>
<properties>
<property name="requireCause" value="true"/>
<property name="forbidLogAndThrow" value="true"/>
<property name="ignoredExceptions" value="InterruptedException,NumberFormatException"/>
</properties>
</rule>
<rule name="HardcodedCredentials"
language="java"
message="Potential hardcoded credential detected"
class="com.example.pmd.rules.HardcodedCredentialsRule">
<description>Detects potential hardcoded credentials</description>
<priority>1</priority>
<properties>
<property name="sensitiveKeys">
<value>password</value>
<value>secret</value>
<value>key</value>
<value>token</value>
<value>credential</value>
<value>auth</value>
</property>
</properties>
</rule>
<rule name="StringConcatenationInLoop"
language="java"
message="String concatenation in loop detected"
class="com.example.pmd.rules.StringConcatenationInLoopRule">
<description>Detects string concatenation in loops</description>
<priority>2</priority>
<properties>
<property name="minIterations" value="3"/>
</properties>
</rule>
</ruleset>
2. Maven PMD Plugin Configuration
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <version>3.20.0</version> <configuration> <rulesets> <ruleset>src/main/resources/rulesets/custom-ruleset.xml</ruleset> </rulesets> <printFailingErrors>true</printFailingErrors> <failurePriority>3</failurePriority> <minimumPriority>5</minimumPriority> <excludes> <exclude>**/test/**</exclude> <exclude>**/generated/**</exclude> </excludes> <includeTests>false</includeTests> <analysisCache>true</analysisCache> </configuration> <executions> <execution> <goals> <goal>check</goal> <goal>cpd-check</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Testing Custom Rules
1. Rule Test Base Class
package com.example.pmd.test;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.JavaLanguageModule;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.testframework.PmdRuleTst;
import net.sourceforge.pmd.testframework.RuleTst;
public abstract class CustomRuleTest extends PmdRuleTst {
protected RuleContext createRuleContext() {
RuleContext context = new RuleContext();
context.setLanguageVersion(JavaLanguageModule.getInstance().getDefaultVersion());
return context;
}
}
2. Method Naming Convention Rule Test
package com.example.pmd.rules;
import com.example.pmd.test.CustomRuleTest;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MethodNamingConventionRuleTest extends CustomRuleTest {
private MethodNamingConventionRule rule;
private RuleContext context;
@BeforeEach
void setUp() {
rule = new MethodNamingConventionRule();
context = createRuleContext();
}
@Test
void testValidMethodNames() {
String code = """
public class TestClass {
public void validMethod() {}
public void anotherValidMethod() {}
private void _privateMethod() {}
}
""";
ASTCompilationUnit ast = parseJava(code);
rule.apply(ast.asList(), context);
assertEquals(0, context.getReport().getViolations().size());
}
@Test
void testInvalidMethodNames() {
String code = """
public class TestClass {
public void InvalidMethod() {} // Starts with capital
public void method_with_underscore() {} // Contains underscore
public void $weirdMethod() {} // Starts with special char
}
""";
ASTCompilationUnit ast = parseJava(code);
rule.apply(ast.asList(), context);
assertEquals(3, context.getReport().getViolations().size());
}
@Test
void testGetterSetterValidation() {
String code = """
public class TestClass {
public String getName() { return name; } // Valid getter
public void setName(String name) { this.name = name; } // Valid setter
public String get() { return ""; } // Invalid getter
public void set() {} // Invalid setter
public void setName() {} // Invalid setter - no param
}
""";
ASTCompilationUnit ast = parseJava(code);
rule.apply(ast.asList(), context);
assertEquals(3, context.getReport().getViolations().size());
}
}
3. XML-based Rule Tests
<?xml version="1.0" encoding="UTF-8"?>
<test-data xmlns="http://pmd.sourceforge.net/rule-tests"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests
https://pmd.sourceforge.io/rule-tests_1_0_0.xsd">
<test-code>
<description>Valid method names</description>
<expected-problems>0</expected-problems>
<code><![CDATA[
public class TestClass {
public void validMethod() {}
public void anotherValidMethod() {}
}
]]></code>
</test-code>
<test-code>
<description>Invalid method names</description>
<expected-problems>2</expected-problems>
<code><![CDATA[
public class TestClass {
public void InvalidMethod() {} // Violation - starts with capital
public void method_with_underscore() {} // Violation - contains underscore
}
]]></code>
</test-code>
<test-code>
<description>Test methods should be ignored</description>
<expected-problems>0</expected-problems>
<code><![CDATA[
public class TestClass {
@Test
public void testSomething() {} // Should be ignored
public void testHelper() {} // Should be checked
}
]]></code>
</test-code>
</test-data>
Best Practices for Custom Rules
- Clear Violation Messages: Provide specific, actionable messages
- Configurable Properties: Make rules flexible through properties
- Performance: Ensure rules don't significantly impact analysis time
- Testing: Comprehensive test coverage for all rule scenarios
- Documentation: Provide clear examples and documentation
- Selective Application: Use excludes for generated code and tests
// Good practice - specific violation message addViolation(data, node, "Method name '" + methodName + "' should follow camelCase convention"); // Bad practice - vague message addViolation(data, node, "Bad method name");
Conclusion
Custom PMD rules provide:
- Team-specific standards enforcement
- Consistent code quality across the codebase
- Early bug detection in development phase
- Automated code review capabilities
- Custom security checks for organization needs
By creating and maintaining custom PMD rules, you can ensure your Java codebase adheres to your organization's specific coding standards, security requirements, and best practices, leading to more maintainable and reliable software.
Advanced Java Supply Chain Security, Kubernetes Hardening & Runtime Threat Detection
Sigstore Rekor in Java – https://macronepal.com/blog/sigstore-rekor-in-java/
Explains integrating Sigstore Rekor into Java systems to create a transparent, tamper-proof log of software signatures and metadata for verifying supply chain integrity.
Securing Java Applications with Chainguard Wolfi – https://macronepal.com/blog/securing-java-applications-with-chainguard-wolfi-a-comprehensive-guide/
Explains using Chainguard Wolfi minimal container images to reduce vulnerabilities and secure Java applications with hardened, lightweight runtime environments.
Cosign Image Signing in Java Complete Guide – https://macronepal.com/blog/cosign-image-signing-in-java-complete-guide/
Explains how to digitally sign container images using Cosign in Java-based workflows to ensure authenticity and prevent unauthorized modifications.
Secure Supply Chain Enforcement Kyverno Image Verification for Java Containers – https://macronepal.com/blog/secure-supply-chain-enforcement-kyverno-image-verification-for-java-containers/
Explains enforcing Kubernetes policies with Kyverno to verify container image signatures and ensure only trusted Java container images are deployed.
Pod Security Admission in Java Securing Kubernetes Deployments for JVM Applications – https://macronepal.com/blog/pod-security-admission-in-java-securing-kubernetes-deployments-for-jvm-applications/
Explains Kubernetes Pod Security Admission policies that enforce security rules like restricted privileges and safe configurations for Java workloads.
Securing Java Applications at Runtime Kubernetes Security Context – https://macronepal.com/blog/securing-java-applications-at-runtime-a-guide-to-kubernetes-security-context/
Explains how Kubernetes security contexts control runtime permissions, user IDs, and access rights for Java containers to improve isolation.
Process Anomaly Detection in Java Behavioral Monitoring – https://macronepal.com/blog/process-anomaly-detection-in-java-comprehensive-behavioral-monitoring-2/
Explains detecting abnormal runtime behavior in Java applications to identify potential security threats using process monitoring techniques.
Achieving Security Excellence CIS Benchmark Compliance for Java Applications – https://macronepal.com/blog/achieving-security-excellence-implementing-cis-benchmark-compliance-for-java-applications/
Explains applying CIS security benchmarks to Java environments to standardize hardening and improve overall system security posture.
Process Anomaly Detection in Java Behavioral Monitoring – https://macronepal.com/blog/process-anomaly-detection-in-java-comprehensive-behavioral-monitoring/
Explains behavioral monitoring of Java processes to detect anomalies and improve runtime security through continuous observation and analysis.
JAVA CODE COMPILER