google-site-verification: google61fe8ba583a51912.html
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