Java language support module for the PMD static code analyzer with AST processing, symbol resolution, type system, metrics, and 400+ built-in rules
—
PMD Java includes a comprehensive rule framework with over 400 built-in rules across 8 categories for detecting code quality issues, security vulnerabilities, and performance problems. The framework supports visitor-based rules, XPath-based custom rules, and provides infrastructure for developing custom rules.
PMD Java organizes its 400+ built-in rules into eight main categories, each targeting specific aspects of code quality:
Rules that enforce coding best practices and prevent common mistakes.
Rules that enforce consistent coding style and formatting conventions.
Rules that detect design problems and architectural issues.
Rules that enforce proper documentation and commenting standards.
Rules that detect patterns that are likely to lead to bugs or runtime errors.
Rules that detect concurrency and thread safety issues.
Rules that identify performance problems and inefficient code patterns.
Rules that detect security vulnerabilities and unsafe practices.
/**
* Base interface for all PMD rules
*/
public interface Rule {
/**
* Returns the name of this rule
*/
String getName();
/**
* Returns the description of this rule
*/
String getDescription();
/**
* Returns the message displayed when this rule is violated
*/
String getMessage();
/**
* Returns the external info URL for this rule
*/
String getExternalInfoUrl();
/**
* Returns the priority/severity of this rule
*/
RulePriority getPriority();
/**
* Applies this rule to the given node and reports violations
*/
void apply(List<? extends Node> nodes, RuleContext ctx);
/**
* Returns the language this rule applies to
*/
Language getLanguage();
/**
* Returns the minimum language version required for this rule
*/
LanguageVersion getMinimumLanguageVersion();
/**
* Returns the maximum language version supported by this rule
*/
LanguageVersion getMaximumLanguageVersion();
}/**
* Base class for Java-specific rules using the visitor pattern
*/
public abstract class AbstractJavaRule extends AbstractRule implements JavaParserVisitor {
/**
* Default implementation that visits child nodes
*/
public Object visit(JavaNode node, Object data);
/**
* Visit method for compilation units (root nodes)
*/
public Object visit(ASTCompilationUnit node, Object data);
/**
* Visit method for class declarations
*/
public Object visit(ASTClassDeclaration node, Object data);
/**
* Visit method for method declarations
*/
public Object visit(ASTMethodDeclaration node, Object data);
/**
* Visit method for field declarations
*/
public Object visit(ASTFieldDeclaration node, Object data);
/**
* Visit method for method calls
*/
public Object visit(ASTMethodCall node, Object data);
/**
* Visit method for variable access
*/
public Object visit(ASTVariableAccess node, Object data);
// 180+ additional visit methods for all AST node types...
/**
* Convenience method to add a rule violation
*/
protected final void addViolation(Object data, JavaNode node);
/**
* Convenience method to add a rule violation with custom message
*/
protected final void addViolation(Object data, JavaNode node, String message);
/**
* Convenience method to add a rule violation with message arguments
*/
protected final void addViolation(Object data, JavaNode node, Object[] args);
}/**
* Context provided to rules during execution
*/
public interface RuleContext {
/**
* Reports a rule violation
*/
void addViolation(RuleViolation violation);
/**
* Gets the current source file being analyzed
*/
TextFile getCurrentFile();
/**
* Gets the language version being processed
*/
LanguageVersion getLanguageVersion();
/**
* Gets configuration for the current rule
*/
RuleSetReferenceId getCurrentRuleSetReferenceId();
}
/**
* Represents a rule violation found in source code
*/
public interface RuleViolation {
/**
* Gets the rule that was violated
*/
Rule getRule();
/**
* Gets the violation message
*/
String getDescription();
/**
* Gets the source file containing the violation
*/
String getFilename();
/**
* Gets the line number of the violation
*/
int getBeginLine();
/**
* Gets the column number of the violation
*/
int getBeginColumn();
/**
* Gets the end line number of the violation
*/
int getEndLine();
/**
* Gets the end column number of the violation
*/
int getEndColumn();
/**
* Gets the priority/severity of the violation
*/
RulePriority getPriority();
}/**
* Example rule that detects empty catch blocks
*/
public class EmptyCatchBlockRule extends AbstractJavaRule {
@Override
public Object visit(ASTTryStatement node, Object data) {
// Check each catch clause
for (ASTCatchClause catchClause : node.getCatchClauses()) {
ASTBlock catchBlock = catchClause.getBody();
// Check if catch block is empty
if (catchBlock.isEmpty()) {
addViolation(data, catchClause, "Empty catch block");
}
}
return super.visit(node, data);
}
}/**
* Example rule that detects unused private methods
*/
public class UnusedPrivateMethodRule extends AbstractJavaRule {
private Set<JMethodSymbol> privateMethods = new HashSet<>();
private Set<JMethodSymbol> calledMethods = new HashSet<>();
@Override
public Object visit(ASTCompilationUnit node, Object data) {
// Reset state for new compilation unit
privateMethods.clear();
calledMethods.clear();
// First pass: collect all private methods
super.visit(node, data);
// Report unused private methods
for (JMethodSymbol method : privateMethods) {
if (!calledMethods.contains(method) && !isSpecialMethod(method)) {
JavaNode methodNode = method.tryGetNode();
if (methodNode != null) {
addViolation(data, methodNode,
"Unused private method: " + method.getSimpleName());
}
}
}
return data;
}
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
JMethodSymbol symbol = node.getSymbol();
// Collect private methods
if (symbol != null && node.getVisibility() == Visibility.PRIVATE) {
privateMethods.add(symbol);
}
return super.visit(node, data);
}
@Override
public Object visit(ASTMethodCall node, Object data) {
JMethodSymbol calledMethod = node.getMethodType();
// Track method calls
if (calledMethod != null && !calledMethod.isUnresolved()) {
calledMethods.add(calledMethod);
}
return super.visit(node, data);
}
private boolean isSpecialMethod(JMethodSymbol method) {
String name = method.getSimpleName();
return "readObject".equals(name) ||
"writeObject".equals(name) ||
"readResolve".equals(name) ||
"writeReplace".equals(name);
}
}/**
* Rule that detects assignment to parameters
*/
public class AvoidReassigningParametersRule extends AbstractJavaRule {
@Override
public Object visit(ASTAssignmentExpression node, Object data) {
// Check if left side is parameter assignment
ASTExpression leftSide = node.getLeftOperand();
if (leftSide instanceof ASTVariableAccess) {
ASTVariableAccess varAccess = (ASTVariableAccess) leftSide;
JVariableSymbol variable = varAccess.getReferencedSym();
// Check if it's a formal parameter
if (variable instanceof JFormalParamSymbol) {
addViolation(data, node,
"Avoid reassigning parameter: " + variable.getSimpleName());
}
}
return super.visit(node, data);
}
}PMD supports XPath expressions for creating rules without writing Java code.
/**
* XPath-based rule implementation
*/
public class XPathRule extends AbstractRule implements JavaParserVisitor {
/**
* Sets the XPath expression for this rule
*/
public void setXPath(String xpath);
/**
* Gets the XPath expression
*/
public String getXPath();
/**
* Sets XPath version (1.0, 2.0, 3.1)
*/
public void setVersion(String version);
/**
* Executes the XPath query against the AST
*/
@Override
public Object visit(JavaNode node, Object data);
}Detect System.out.println usage:
//ASTMethodCall[
@MethodName = 'println' and
ASTFieldAccess[@Name = 'out' and ASTFieldAccess[@Name = 'System']]
]Find methods with too many parameters:
//ASTMethodDeclaration[count(ASTFormalParameters/ASTFormalParameter) > 5]Detect empty if blocks:
//ASTIfStatement[ASTBlock[count(*) = 0]]Find catch blocks that only log and rethrow:
//ASTCatchClause[
count(ASTBlock/*) = 2 and
ASTBlock/ASTExpressionStatement/ASTMethodCall[@MethodName = 'log'] and
ASTBlock/ASTThrowStatement
]/**
* Main entry point for rule execution
*/
public class JavaLanguageModule extends LanguageModuleBase {
/**
* Creates a language processor for executing rules
*/
@Override
public LanguageProcessor createProcessor(LanguagePropertyBundle bundle) {
return new JavaLanguageProcessor(bundle);
}
}
/**
* Language processor that coordinates rule execution
*/
public class JavaLanguageProcessor implements LanguageProcessor {
/**
* Processes source files and applies rules
*/
public void processSource(TextFile textFile, RuleSet ruleSet, RuleContext ruleContext);
/**
* Creates services for symbol resolution and type checking
*/
public AnalysisServices services();
}
/**
* Services available during rule execution
*/
public interface AnalysisServices {
/**
* Gets the symbol resolver for the current analysis
*/
SymbolResolver getSymbolResolver();
/**
* Gets the type resolver for the current analysis
*/
TypeResolver getTypeResolver();
/**
* Gets the semantic model (symbol table + type system)
*/
SemanticModel getSemanticModel();
}/**
* Extended context for Java rule execution
*/
public interface JavaRuleContext extends RuleContext {
/**
* Gets the parsed AST for the current file
*/
ASTCompilationUnit getCompilationUnit();
/**
* Gets the symbol table for symbol resolution
*/
JSymbolTable getSymbolTable();
/**
* Gets the type system for type analysis
*/
TypeSystem getTypeSystem();
/**
* Gets semantic analysis services
*/
AnalysisServices getAnalysisServices();
}/**
* Base class for rule properties
*/
public abstract class PropertyDescriptor<T> {
/**
* Gets the name of this property
*/
public String name();
/**
* Gets the description of this property
*/
public String description();
/**
* Gets the default value
*/
public T defaultValue();
/**
* Validates a property value
*/
public String errorFor(T value);
}
/**
* Property descriptor for integer values
*/
public class IntegerPropertyDescriptor extends PropertyDescriptor<Integer> {
/**
* Creates descriptor with range validation
*/
public static IntegerPropertyDescriptor named(String name)
.desc(String description)
.range(int min, int max)
.defaultValue(int defaultValue);
}
/**
* Property descriptor for string values
*/
public class StringPropertyDescriptor extends PropertyDescriptor<String> {
/**
* Creates descriptor with regex validation
*/
public static StringPropertyDescriptor named(String name)
.desc(String description)
.validValues(String... values)
.defaultValue(String defaultValue);
}/**
* Rule with configurable properties
*/
public class CyclomaticComplexityRule extends AbstractJavaRule {
// Property descriptors
private static final IntegerPropertyDescriptor CYCLO_THRESHOLD =
IntegerPropertyDescriptor.named("cyclomaticComplexity")
.desc("Maximum cyclomatic complexity before reporting violation")
.range(1, 50)
.defaultValue(10);
private static final BooleanPropertyDescriptor IGNORE_BOOLEAN_PATHS =
BooleanPropertyDescriptor.named("ignoreBooleanPaths")
.desc("Ignore boolean expressions in complexity calculation")
.defaultValue(false);
// Define properties for this rule
public CyclomaticComplexityRule() {
definePropertyDescriptor(CYCLO_THRESHOLD);
definePropertyDescriptor(IGNORE_BOOLEAN_PATHS);
}
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
// Get configured property values
int threshold = getProperty(CYCLO_THRESHOLD);
boolean ignoreBooleanPaths = getProperty(IGNORE_BOOLEAN_PATHS);
// Create metric options based on properties
MetricOptions options = ignoreBooleanPaths ?
MetricOptions.ofOption(CycloOption.IGNORE_BOOLEAN_PATHS) :
MetricOptions.emptyOptions();
// Calculate complexity
int complexity = JavaMetrics.CYCLO.computeFor(node, options);
if (complexity > threshold) {
addViolation(data, node, new Object[] {
node.getName(),
complexity,
threshold
});
}
return super.visit(node, data);
}
}Key rules in the best practices category:
Critical rules that detect likely bugs:
Rules that identify performance issues:
Rules that detect security vulnerabilities:
// Example: Rule to detect long parameter lists
public class TooManyParametersRule extends AbstractJavaRule {
private static final IntegerPropertyDescriptor MAX_PARAMETERS =
IntegerPropertyDescriptor.named("maxParameters")
.desc("Maximum number of parameters allowed")
.range(1, 20)
.defaultValue(7);
public TooManyParametersRule() {
definePropertyDescriptor(MAX_PARAMETERS);
}
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
int maxParams = getProperty(MAX_PARAMETERS);
ASTFormalParameters params = node.getFormalParameters();
int paramCount = params.size();
if (paramCount > maxParams) {
addViolation(data, node, new Object[] {
node.getName(),
paramCount,
maxParams
});
}
return super.visit(node, data);
}
@Override
public Object visit(ASTConstructorDeclaration node, Object data) {
int maxParams = getProperty(MAX_PARAMETERS);
ASTFormalParameters params = node.getFormalParameters();
int paramCount = params.size();
if (paramCount > maxParams) {
addViolation(data, node, new Object[] {
"constructor",
paramCount,
maxParams
});
}
return super.visit(node, data);
}
}// Example: Rule to detect fields that should be local variables
public class FieldShouldBeLocalRule extends AbstractJavaRule {
private Map<JFieldSymbol, ASTFieldDeclaration> privateFields = new HashMap<>();
private Set<JFieldSymbol> accessedFields = new HashSet<>();
@Override
public Object visit(ASTCompilationUnit node, Object data) {
privateFields.clear();
accessedFields.clear();
// First pass: collect field info and usage
super.visit(node, data);
// Report fields that are only used in one method
for (Map.Entry<JFieldSymbol, ASTFieldDeclaration> entry : privateFields.entrySet()) {
JFieldSymbol field = entry.getKey();
ASTFieldDeclaration fieldDecl = entry.getValue();
if (!field.isStatic() && !accessedFields.contains(field)) {
Set<ASTMethodDeclaration> usingMethods = findMethodsUsingField(node, field);
if (usingMethods.size() == 1) {
addViolation(data, fieldDecl,
"Field '" + field.getSimpleName() +
"' is only used in one method and should be a local variable");
}
}
}
return data;
}
@Override
public Object visit(ASTFieldDeclaration node, Object data) {
if (node.getVisibility() == Visibility.PRIVATE) {
for (ASTVariableId varId : node.getVarIds()) {
JVariableSymbol symbol = varId.getSymbol();
if (symbol instanceof JFieldSymbol) {
privateFields.put((JFieldSymbol) symbol, node);
}
}
}
return super.visit(node, data);
}
@Override
public Object visit(ASTFieldAccess node, Object data) {
JFieldSymbol field = node.getReferencedSym();
if (field != null && privateFields.containsKey(field)) {
accessedFields.add(field);
}
return super.visit(node, data);
}
private Set<ASTMethodDeclaration> findMethodsUsingField(ASTCompilationUnit cu, JFieldSymbol field) {
FieldUsageVisitor visitor = new FieldUsageVisitor(field);
cu.acceptVisitor(visitor, null);
return visitor.getUsingMethods();
}
private static class FieldUsageVisitor extends JavaVisitorBase<Void, Void> {
private final JFieldSymbol targetField;
private final Set<ASTMethodDeclaration> usingMethods = new HashSet<>();
private ASTMethodDeclaration currentMethod;
public FieldUsageVisitor(JFieldSymbol field) {
this.targetField = field;
}
@Override
public Void visit(ASTMethodDeclaration node, Void data) {
ASTMethodDeclaration oldMethod = currentMethod;
currentMethod = node;
super.visit(node, data);
currentMethod = oldMethod;
return null;
}
@Override
public Void visit(ASTFieldAccess node, Void data) {
if (targetField.equals(node.getReferencedSym()) && currentMethod != null) {
usingMethods.add(currentMethod);
}
return super.visit(node, data);
}
public Set<ASTMethodDeclaration> getUsingMethods() {
return usingMethods;
}
}
}// Example: Creating XPath rules programmatically
public class CustomXPathRules {
public static Rule createEmptyIfRule() {
XPathRule rule = new XPathRule();
rule.setName("EmptyIfStatement");
rule.setMessage("Empty if statement");
rule.setDescription("Detects empty if statements");
rule.setXPath("//ASTIfStatement[ASTBlock[count(*) = 0]]");
rule.setLanguage(JavaLanguageModule.getInstance());
return rule;
}
public static Rule createTooManyParametersRule() {
XPathRule rule = new XPathRule();
rule.setName("TooManyParameters");
rule.setMessage("Too many parameters: {0}");
rule.setDescription("Methods should not have too many parameters");
rule.setXPath("//ASTMethodDeclaration[count(.//ASTFormalParameter) > 7]");
rule.setLanguage(JavaLanguageModule.getInstance());
return rule;
}
public static Rule createStringLiteralEqualityRule() {
XPathRule rule = new XPathRule();
rule.setName("StringLiteralEquality");
rule.setMessage("Use equals() instead of == for string comparison");
rule.setDescription("String literals should be compared using equals() method");
rule.setXPath("""
//ASTEqualityExpression[@Operator = '==' or @Operator = '!=']
[ASTStringLiteral or
.//*[self::ASTMethodCall[@MethodName = 'toString'] or
self::ASTMethodCall[@MethodName = 'trim'] or
self::ASTMethodCall[@MethodName = 'toLowerCase'] or
self::ASTMethodCall[@MethodName = 'toUpperCase']]]
""");
rule.setLanguage(JavaLanguageModule.getInstance());
return rule;
}
}Install with Tessl CLI
npx tessl i tessl/maven-net-sourceforge-pmd--pmd-java