0
# Rule Framework
1
2
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.
3
4
## Capabilities
5
6
### Rule Categories
7
8
PMD Java organizes its 400+ built-in rules into eight main categories, each targeting specific aspects of code quality:
9
10
#### Best Practices (80+ rules)
11
Rules that enforce coding best practices and prevent common mistakes.
12
13
#### Code Style (60+ rules)
14
Rules that enforce consistent coding style and formatting conventions.
15
16
#### Design (30+ rules)
17
Rules that detect design problems and architectural issues.
18
19
#### Documentation (5+ rules)
20
Rules that enforce proper documentation and commenting standards.
21
22
#### Error Prone (120+ rules)
23
Rules that detect patterns that are likely to lead to bugs or runtime errors.
24
25
#### Multithreading (15+ rules)
26
Rules that detect concurrency and thread safety issues.
27
28
#### Performance (25+ rules)
29
Rules that identify performance problems and inefficient code patterns.
30
31
#### Security (5+ rules)
32
Rules that detect security vulnerabilities and unsafe practices.
33
34
### Rule Framework Architecture
35
36
```java { .api }
37
/**
38
* Base interface for all PMD rules
39
*/
40
public interface Rule {
41
/**
42
* Returns the name of this rule
43
*/
44
String getName();
45
46
/**
47
* Returns the description of this rule
48
*/
49
String getDescription();
50
51
/**
52
* Returns the message displayed when this rule is violated
53
*/
54
String getMessage();
55
56
/**
57
* Returns the external info URL for this rule
58
*/
59
String getExternalInfoUrl();
60
61
/**
62
* Returns the priority/severity of this rule
63
*/
64
RulePriority getPriority();
65
66
/**
67
* Applies this rule to the given node and reports violations
68
*/
69
void apply(List<? extends Node> nodes, RuleContext ctx);
70
71
/**
72
* Returns the language this rule applies to
73
*/
74
Language getLanguage();
75
76
/**
77
* Returns the minimum language version required for this rule
78
*/
79
LanguageVersion getMinimumLanguageVersion();
80
81
/**
82
* Returns the maximum language version supported by this rule
83
*/
84
LanguageVersion getMaximumLanguageVersion();
85
}
86
```
87
88
### Java Rule Base Class
89
90
```java { .api }
91
/**
92
* Base class for Java-specific rules using the visitor pattern
93
*/
94
public abstract class AbstractJavaRule extends AbstractRule implements JavaParserVisitor {
95
/**
96
* Default implementation that visits child nodes
97
*/
98
public Object visit(JavaNode node, Object data);
99
100
/**
101
* Visit method for compilation units (root nodes)
102
*/
103
public Object visit(ASTCompilationUnit node, Object data);
104
105
/**
106
* Visit method for class declarations
107
*/
108
public Object visit(ASTClassDeclaration node, Object data);
109
110
/**
111
* Visit method for method declarations
112
*/
113
public Object visit(ASTMethodDeclaration node, Object data);
114
115
/**
116
* Visit method for field declarations
117
*/
118
public Object visit(ASTFieldDeclaration node, Object data);
119
120
/**
121
* Visit method for method calls
122
*/
123
public Object visit(ASTMethodCall node, Object data);
124
125
/**
126
* Visit method for variable access
127
*/
128
public Object visit(ASTVariableAccess node, Object data);
129
130
// 180+ additional visit methods for all AST node types...
131
132
/**
133
* Convenience method to add a rule violation
134
*/
135
protected final void addViolation(Object data, JavaNode node);
136
137
/**
138
* Convenience method to add a rule violation with custom message
139
*/
140
protected final void addViolation(Object data, JavaNode node, String message);
141
142
/**
143
* Convenience method to add a rule violation with message arguments
144
*/
145
protected final void addViolation(Object data, JavaNode node, Object[] args);
146
}
147
```
148
149
### Rule Context and Violations
150
151
```java { .api }
152
/**
153
* Context provided to rules during execution
154
*/
155
public interface RuleContext {
156
/**
157
* Reports a rule violation
158
*/
159
void addViolation(RuleViolation violation);
160
161
/**
162
* Gets the current source file being analyzed
163
*/
164
TextFile getCurrentFile();
165
166
/**
167
* Gets the language version being processed
168
*/
169
LanguageVersion getLanguageVersion();
170
171
/**
172
* Gets configuration for the current rule
173
*/
174
RuleSetReferenceId getCurrentRuleSetReferenceId();
175
}
176
177
/**
178
* Represents a rule violation found in source code
179
*/
180
public interface RuleViolation {
181
/**
182
* Gets the rule that was violated
183
*/
184
Rule getRule();
185
186
/**
187
* Gets the violation message
188
*/
189
String getDescription();
190
191
/**
192
* Gets the source file containing the violation
193
*/
194
String getFilename();
195
196
/**
197
* Gets the line number of the violation
198
*/
199
int getBeginLine();
200
201
/**
202
* Gets the column number of the violation
203
*/
204
int getBeginColumn();
205
206
/**
207
* Gets the end line number of the violation
208
*/
209
int getEndLine();
210
211
/**
212
* Gets the end column number of the violation
213
*/
214
int getEndColumn();
215
216
/**
217
* Gets the priority/severity of the violation
218
*/
219
RulePriority getPriority();
220
}
221
```
222
223
## Visitor-Based Rules
224
225
### Simple Rule Example
226
227
```java { .api }
228
/**
229
* Example rule that detects empty catch blocks
230
*/
231
public class EmptyCatchBlockRule extends AbstractJavaRule {
232
233
@Override
234
public Object visit(ASTTryStatement node, Object data) {
235
// Check each catch clause
236
for (ASTCatchClause catchClause : node.getCatchClauses()) {
237
ASTBlock catchBlock = catchClause.getBody();
238
239
// Check if catch block is empty
240
if (catchBlock.isEmpty()) {
241
addViolation(data, catchClause, "Empty catch block");
242
}
243
}
244
245
return super.visit(node, data);
246
}
247
}
248
```
249
250
### Complex Rule with Symbol Resolution
251
252
```java { .api }
253
/**
254
* Example rule that detects unused private methods
255
*/
256
public class UnusedPrivateMethodRule extends AbstractJavaRule {
257
private Set<JMethodSymbol> privateMethods = new HashSet<>();
258
private Set<JMethodSymbol> calledMethods = new HashSet<>();
259
260
@Override
261
public Object visit(ASTCompilationUnit node, Object data) {
262
// Reset state for new compilation unit
263
privateMethods.clear();
264
calledMethods.clear();
265
266
// First pass: collect all private methods
267
super.visit(node, data);
268
269
// Report unused private methods
270
for (JMethodSymbol method : privateMethods) {
271
if (!calledMethods.contains(method) && !isSpecialMethod(method)) {
272
JavaNode methodNode = method.tryGetNode();
273
if (methodNode != null) {
274
addViolation(data, methodNode,
275
"Unused private method: " + method.getSimpleName());
276
}
277
}
278
}
279
280
return data;
281
}
282
283
@Override
284
public Object visit(ASTMethodDeclaration node, Object data) {
285
JMethodSymbol symbol = node.getSymbol();
286
287
// Collect private methods
288
if (symbol != null && node.getVisibility() == Visibility.PRIVATE) {
289
privateMethods.add(symbol);
290
}
291
292
return super.visit(node, data);
293
}
294
295
@Override
296
public Object visit(ASTMethodCall node, Object data) {
297
JMethodSymbol calledMethod = node.getMethodType();
298
299
// Track method calls
300
if (calledMethod != null && !calledMethod.isUnresolved()) {
301
calledMethods.add(calledMethod);
302
}
303
304
return super.visit(node, data);
305
}
306
307
private boolean isSpecialMethod(JMethodSymbol method) {
308
String name = method.getSimpleName();
309
return "readObject".equals(name) ||
310
"writeObject".equals(name) ||
311
"readResolve".equals(name) ||
312
"writeReplace".equals(name);
313
}
314
}
315
```
316
317
### Rules Using Type System
318
319
```java { .api }
320
/**
321
* Rule that detects assignment to parameters
322
*/
323
public class AvoidReassigningParametersRule extends AbstractJavaRule {
324
325
@Override
326
public Object visit(ASTAssignmentExpression node, Object data) {
327
// Check if left side is parameter assignment
328
ASTExpression leftSide = node.getLeftOperand();
329
330
if (leftSide instanceof ASTVariableAccess) {
331
ASTVariableAccess varAccess = (ASTVariableAccess) leftSide;
332
JVariableSymbol variable = varAccess.getReferencedSym();
333
334
// Check if it's a formal parameter
335
if (variable instanceof JFormalParamSymbol) {
336
addViolation(data, node,
337
"Avoid reassigning parameter: " + variable.getSimpleName());
338
}
339
}
340
341
return super.visit(node, data);
342
}
343
}
344
```
345
346
## XPath-Based Rules
347
348
PMD supports XPath expressions for creating rules without writing Java code.
349
350
### XPath Rule Configuration
351
352
```java { .api }
353
/**
354
* XPath-based rule implementation
355
*/
356
public class XPathRule extends AbstractRule implements JavaParserVisitor {
357
/**
358
* Sets the XPath expression for this rule
359
*/
360
public void setXPath(String xpath);
361
362
/**
363
* Gets the XPath expression
364
*/
365
public String getXPath();
366
367
/**
368
* Sets XPath version (1.0, 2.0, 3.1)
369
*/
370
public void setVersion(String version);
371
372
/**
373
* Executes the XPath query against the AST
374
*/
375
@Override
376
public Object visit(JavaNode node, Object data);
377
}
378
```
379
380
### XPath Rule Examples
381
382
**Detect System.out.println usage:**
383
```xpath
384
//ASTMethodCall[
385
@MethodName = 'println' and
386
ASTFieldAccess[@Name = 'out' and ASTFieldAccess[@Name = 'System']]
387
]
388
```
389
390
**Find methods with too many parameters:**
391
```xpath
392
//ASTMethodDeclaration[count(ASTFormalParameters/ASTFormalParameter) > 5]
393
```
394
395
**Detect empty if blocks:**
396
```xpath
397
//ASTIfStatement[ASTBlock[count(*) = 0]]
398
```
399
400
**Find catch blocks that only log and rethrow:**
401
```xpath
402
//ASTCatchClause[
403
count(ASTBlock/*) = 2 and
404
ASTBlock/ASTExpressionStatement/ASTMethodCall[@MethodName = 'log'] and
405
ASTBlock/ASTThrowStatement
406
]
407
```
408
409
## Rule Execution Framework
410
411
### Rule Processing Pipeline
412
413
```java { .api }
414
/**
415
* Main entry point for rule execution
416
*/
417
public class JavaLanguageModule extends LanguageModuleBase {
418
/**
419
* Creates a language processor for executing rules
420
*/
421
@Override
422
public LanguageProcessor createProcessor(LanguagePropertyBundle bundle) {
423
return new JavaLanguageProcessor(bundle);
424
}
425
}
426
427
/**
428
* Language processor that coordinates rule execution
429
*/
430
public class JavaLanguageProcessor implements LanguageProcessor {
431
/**
432
* Processes source files and applies rules
433
*/
434
public void processSource(TextFile textFile, RuleSet ruleSet, RuleContext ruleContext);
435
436
/**
437
* Creates services for symbol resolution and type checking
438
*/
439
public AnalysisServices services();
440
}
441
442
/**
443
* Services available during rule execution
444
*/
445
public interface AnalysisServices {
446
/**
447
* Gets the symbol resolver for the current analysis
448
*/
449
SymbolResolver getSymbolResolver();
450
451
/**
452
* Gets the type resolver for the current analysis
453
*/
454
TypeResolver getTypeResolver();
455
456
/**
457
* Gets the semantic model (symbol table + type system)
458
*/
459
SemanticModel getSemanticModel();
460
}
461
```
462
463
### Rule Execution Context
464
465
```java { .api }
466
/**
467
* Extended context for Java rule execution
468
*/
469
public interface JavaRuleContext extends RuleContext {
470
/**
471
* Gets the parsed AST for the current file
472
*/
473
ASTCompilationUnit getCompilationUnit();
474
475
/**
476
* Gets the symbol table for symbol resolution
477
*/
478
JSymbolTable getSymbolTable();
479
480
/**
481
* Gets the type system for type analysis
482
*/
483
TypeSystem getTypeSystem();
484
485
/**
486
* Gets semantic analysis services
487
*/
488
AnalysisServices getAnalysisServices();
489
}
490
```
491
492
## Rule Configuration
493
494
### Rule Properties
495
496
```java { .api }
497
/**
498
* Base class for rule properties
499
*/
500
public abstract class PropertyDescriptor<T> {
501
/**
502
* Gets the name of this property
503
*/
504
public String name();
505
506
/**
507
* Gets the description of this property
508
*/
509
public String description();
510
511
/**
512
* Gets the default value
513
*/
514
public T defaultValue();
515
516
/**
517
* Validates a property value
518
*/
519
public String errorFor(T value);
520
}
521
522
/**
523
* Property descriptor for integer values
524
*/
525
public class IntegerPropertyDescriptor extends PropertyDescriptor<Integer> {
526
/**
527
* Creates descriptor with range validation
528
*/
529
public static IntegerPropertyDescriptor named(String name)
530
.desc(String description)
531
.range(int min, int max)
532
.defaultValue(int defaultValue);
533
}
534
535
/**
536
* Property descriptor for string values
537
*/
538
public class StringPropertyDescriptor extends PropertyDescriptor<String> {
539
/**
540
* Creates descriptor with regex validation
541
*/
542
public static StringPropertyDescriptor named(String name)
543
.desc(String description)
544
.validValues(String... values)
545
.defaultValue(String defaultValue);
546
}
547
```
548
549
### Configurable Rule Example
550
551
```java { .api }
552
/**
553
* Rule with configurable properties
554
*/
555
public class CyclomaticComplexityRule extends AbstractJavaRule {
556
557
// Property descriptors
558
private static final IntegerPropertyDescriptor CYCLO_THRESHOLD =
559
IntegerPropertyDescriptor.named("cyclomaticComplexity")
560
.desc("Maximum cyclomatic complexity before reporting violation")
561
.range(1, 50)
562
.defaultValue(10);
563
564
private static final BooleanPropertyDescriptor IGNORE_BOOLEAN_PATHS =
565
BooleanPropertyDescriptor.named("ignoreBooleanPaths")
566
.desc("Ignore boolean expressions in complexity calculation")
567
.defaultValue(false);
568
569
// Define properties for this rule
570
public CyclomaticComplexityRule() {
571
definePropertyDescriptor(CYCLO_THRESHOLD);
572
definePropertyDescriptor(IGNORE_BOOLEAN_PATHS);
573
}
574
575
@Override
576
public Object visit(ASTMethodDeclaration node, Object data) {
577
// Get configured property values
578
int threshold = getProperty(CYCLO_THRESHOLD);
579
boolean ignoreBooleanPaths = getProperty(IGNORE_BOOLEAN_PATHS);
580
581
// Create metric options based on properties
582
MetricOptions options = ignoreBooleanPaths ?
583
MetricOptions.ofOption(CycloOption.IGNORE_BOOLEAN_PATHS) :
584
MetricOptions.emptyOptions();
585
586
// Calculate complexity
587
int complexity = JavaMetrics.CYCLO.computeFor(node, options);
588
589
if (complexity > threshold) {
590
addViolation(data, node, new Object[] {
591
node.getName(),
592
complexity,
593
threshold
594
});
595
}
596
597
return super.visit(node, data);
598
}
599
}
600
```
601
602
## Built-in Rule Categories
603
604
### Best Practices Rules
605
606
Key rules in the best practices category:
607
608
- **AvoidReassigningParameters**: Avoid reassigning method parameters
609
- **OneDeclarationPerLine**: Declare variables one per line
610
- **UseCollectionIsEmpty**: Use isEmpty() instead of size() == 0
611
- **SystemPrintln**: Avoid System.out.println in production code
612
- **UnusedImports**: Remove unused import statements
613
- **UnusedLocalVariable**: Remove unused local variables
614
- **UnusedPrivateField**: Remove unused private fields
615
- **UnusedPrivateMethod**: Remove unused private methods
616
- **AvoidStringBufferField**: StringBuffer fields should be local variables
617
618
### Error Prone Rules
619
620
Critical rules that detect likely bugs:
621
622
- **AvoidBranchingStatementAsLastInLoop**: Avoid break/continue as last statement
623
- **AvoidDecimalLiteralsInBigDecimalConstructor**: Use string constructor for BigDecimal
624
- **BrokenNullCheck**: Detect broken null checks
625
- **EmptyCatchBlock**: Avoid empty catch blocks
626
- **NullPointerException**: Detect potential NPE scenarios
627
- **UseEqualsToCompareStrings**: Use equals() for string comparison
628
- **MissingBreakInSwitch**: Detect missing break in switch cases
629
- **OverrideBothEqualsAndHashcode**: Override both equals() and hashCode()
630
631
### Performance Rules
632
633
Rules that identify performance issues:
634
635
- **AvoidInstantiatingObjectsInLoops**: Avoid creating objects in loops
636
- **StringInstantiation**: Use string literals instead of new String()
637
- **UseStringBufferForStringAppends**: Use StringBuilder for concatenation
638
- **AvoidArrayLoops**: Use System.arraycopy() for array copying
639
- **BigIntegerInstantiation**: Use BigInteger constants for common values
640
- **BooleanInstantiation**: Use Boolean.TRUE/FALSE constants
641
642
### Security Rules
643
644
Rules that detect security vulnerabilities:
645
646
- **HardCodedCryptoKey**: Avoid hard-coded cryptographic keys
647
- **InsecureCryptoIv**: Use secure initialization vectors
648
- **AvoidFileStream**: Use nio.file APIs for file operations
649
- **StaticEJBFieldShouldBeFinal**: EJB static fields should be final
650
- **DoNotCallGarbageCollection**: Avoid System.gc() calls
651
652
## Usage Examples
653
654
### Creating a Custom Rule
655
656
```java
657
// Example: Rule to detect long parameter lists
658
public class TooManyParametersRule extends AbstractJavaRule {
659
660
private static final IntegerPropertyDescriptor MAX_PARAMETERS =
661
IntegerPropertyDescriptor.named("maxParameters")
662
.desc("Maximum number of parameters allowed")
663
.range(1, 20)
664
.defaultValue(7);
665
666
public TooManyParametersRule() {
667
definePropertyDescriptor(MAX_PARAMETERS);
668
}
669
670
@Override
671
public Object visit(ASTMethodDeclaration node, Object data) {
672
int maxParams = getProperty(MAX_PARAMETERS);
673
ASTFormalParameters params = node.getFormalParameters();
674
675
int paramCount = params.size();
676
if (paramCount > maxParams) {
677
addViolation(data, node, new Object[] {
678
node.getName(),
679
paramCount,
680
maxParams
681
});
682
}
683
684
return super.visit(node, data);
685
}
686
687
@Override
688
public Object visit(ASTConstructorDeclaration node, Object data) {
689
int maxParams = getProperty(MAX_PARAMETERS);
690
ASTFormalParameters params = node.getFormalParameters();
691
692
int paramCount = params.size();
693
if (paramCount > maxParams) {
694
addViolation(data, node, new Object[] {
695
"constructor",
696
paramCount,
697
maxParams
698
});
699
}
700
701
return super.visit(node, data);
702
}
703
}
704
```
705
706
### Rule with Symbol Analysis
707
708
```java
709
// Example: Rule to detect fields that should be local variables
710
public class FieldShouldBeLocalRule extends AbstractJavaRule {
711
private Map<JFieldSymbol, ASTFieldDeclaration> privateFields = new HashMap<>();
712
private Set<JFieldSymbol> accessedFields = new HashSet<>();
713
714
@Override
715
public Object visit(ASTCompilationUnit node, Object data) {
716
privateFields.clear();
717
accessedFields.clear();
718
719
// First pass: collect field info and usage
720
super.visit(node, data);
721
722
// Report fields that are only used in one method
723
for (Map.Entry<JFieldSymbol, ASTFieldDeclaration> entry : privateFields.entrySet()) {
724
JFieldSymbol field = entry.getKey();
725
ASTFieldDeclaration fieldDecl = entry.getValue();
726
727
if (!field.isStatic() && !accessedFields.contains(field)) {
728
Set<ASTMethodDeclaration> usingMethods = findMethodsUsingField(node, field);
729
730
if (usingMethods.size() == 1) {
731
addViolation(data, fieldDecl,
732
"Field '" + field.getSimpleName() +
733
"' is only used in one method and should be a local variable");
734
}
735
}
736
}
737
738
return data;
739
}
740
741
@Override
742
public Object visit(ASTFieldDeclaration node, Object data) {
743
if (node.getVisibility() == Visibility.PRIVATE) {
744
for (ASTVariableId varId : node.getVarIds()) {
745
JVariableSymbol symbol = varId.getSymbol();
746
if (symbol instanceof JFieldSymbol) {
747
privateFields.put((JFieldSymbol) symbol, node);
748
}
749
}
750
}
751
752
return super.visit(node, data);
753
}
754
755
@Override
756
public Object visit(ASTFieldAccess node, Object data) {
757
JFieldSymbol field = node.getReferencedSym();
758
if (field != null && privateFields.containsKey(field)) {
759
accessedFields.add(field);
760
}
761
762
return super.visit(node, data);
763
}
764
765
private Set<ASTMethodDeclaration> findMethodsUsingField(ASTCompilationUnit cu, JFieldSymbol field) {
766
FieldUsageVisitor visitor = new FieldUsageVisitor(field);
767
cu.acceptVisitor(visitor, null);
768
return visitor.getUsingMethods();
769
}
770
771
private static class FieldUsageVisitor extends JavaVisitorBase<Void, Void> {
772
private final JFieldSymbol targetField;
773
private final Set<ASTMethodDeclaration> usingMethods = new HashSet<>();
774
private ASTMethodDeclaration currentMethod;
775
776
public FieldUsageVisitor(JFieldSymbol field) {
777
this.targetField = field;
778
}
779
780
@Override
781
public Void visit(ASTMethodDeclaration node, Void data) {
782
ASTMethodDeclaration oldMethod = currentMethod;
783
currentMethod = node;
784
super.visit(node, data);
785
currentMethod = oldMethod;
786
return null;
787
}
788
789
@Override
790
public Void visit(ASTFieldAccess node, Void data) {
791
if (targetField.equals(node.getReferencedSym()) && currentMethod != null) {
792
usingMethods.add(currentMethod);
793
}
794
return super.visit(node, data);
795
}
796
797
public Set<ASTMethodDeclaration> getUsingMethods() {
798
return usingMethods;
799
}
800
}
801
}
802
```
803
804
### XPath Rule Registration
805
806
```java
807
// Example: Creating XPath rules programmatically
808
public class CustomXPathRules {
809
810
public static Rule createEmptyIfRule() {
811
XPathRule rule = new XPathRule();
812
rule.setName("EmptyIfStatement");
813
rule.setMessage("Empty if statement");
814
rule.setDescription("Detects empty if statements");
815
rule.setXPath("//ASTIfStatement[ASTBlock[count(*) = 0]]");
816
rule.setLanguage(JavaLanguageModule.getInstance());
817
return rule;
818
}
819
820
public static Rule createTooManyParametersRule() {
821
XPathRule rule = new XPathRule();
822
rule.setName("TooManyParameters");
823
rule.setMessage("Too many parameters: {0}");
824
rule.setDescription("Methods should not have too many parameters");
825
rule.setXPath("//ASTMethodDeclaration[count(.//ASTFormalParameter) > 7]");
826
rule.setLanguage(JavaLanguageModule.getInstance());
827
return rule;
828
}
829
830
public static Rule createStringLiteralEqualityRule() {
831
XPathRule rule = new XPathRule();
832
rule.setName("StringLiteralEquality");
833
rule.setMessage("Use equals() instead of == for string comparison");
834
rule.setDescription("String literals should be compared using equals() method");
835
rule.setXPath("""
836
//ASTEqualityExpression[@Operator = '==' or @Operator = '!=']
837
[ASTStringLiteral or
838
.//*[self::ASTMethodCall[@MethodName = 'toString'] or
839
self::ASTMethodCall[@MethodName = 'trim'] or
840
self::ASTMethodCall[@MethodName = 'toLowerCase'] or
841
self::ASTMethodCall[@MethodName = 'toUpperCase']]]
842
""");
843
rule.setLanguage(JavaLanguageModule.getInstance());
844
return rule;
845
}
846
}
847
```