0
# Rule Development
1
2
Rule development framework provides the foundation for creating custom static analysis rules for Scala code. The framework integrates with PMD's rule engine and provides Scala-specific base classes and visitor patterns for comprehensive code analysis.
3
4
## Core Rule Development Components
5
6
### ScalaRule Base Class
7
8
Primary base class for developing Scala-specific PMD rules with integrated visitor pattern support.
9
10
```java { .api }
11
public class ScalaRule extends AbstractRule implements ScalaVisitor<RuleContext, RuleContext> {
12
public void apply(Node target, RuleContext ctx);
13
public RuleContext visitNode(Node node, RuleContext param);
14
15
// Default visitor implementations for all node types
16
public RuleContext visit(ScalaNode<?> node, RuleContext data);
17
public RuleContext visit(ASTSource node, RuleContext data);
18
19
// Override specific visit methods for targeted analysis
20
public RuleContext visit(ASTDefnClass node, RuleContext data);
21
public RuleContext visit(ASTDefnDef node, RuleContext data);
22
public RuleContext visit(ASTTermApply node, RuleContext data);
23
// ... methods for all 140+ AST node types
24
}
25
```
26
27
**Basic Rule Implementation**:
28
29
```java
30
public class ClassComplexityRule extends ScalaRule {
31
private static final int MAX_METHODS = 15;
32
33
@Override
34
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
35
// Count methods in this class
36
long methodCount = node.descendants(ASTDefnDef.class).count();
37
38
if (methodCount > MAX_METHODS) {
39
ctx.addViolation(node,
40
"Class '" + getClassName(node) + "' has too many methods: " +
41
methodCount + " (max: " + MAX_METHODS + ")");
42
}
43
44
// Continue visiting child nodes
45
return super.visit(node, ctx);
46
}
47
48
private String getClassName(ASTDefnClass classNode) {
49
// Extract class name from AST node
50
return classNode.descendants(ASTTermName.class)
51
.findFirst()
52
.map(name -> name.getText())
53
.orElse("Unknown");
54
}
55
}
56
```
57
58
## Rule Implementation Patterns
59
60
### Method-Level Analysis Rules
61
62
```java
63
public class MethodNamingRule extends ScalaRule {
64
private static final Pattern CAMEL_CASE = Pattern.compile("^[a-z][a-zA-Z0-9]*$");
65
66
@Override
67
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
68
String methodName = extractMethodName(node);
69
70
if (!CAMEL_CASE.matcher(methodName).matches()) {
71
ctx.addViolation(node,
72
"Method name '" + methodName + "' should be in camelCase");
73
}
74
75
// Check method length
76
int lineCount = node.getEndLine() - node.getBeginLine() + 1;
77
if (lineCount > 50) {
78
ctx.addViolation(node,
79
"Method '" + methodName + "' is too long: " + lineCount + " lines");
80
}
81
82
return super.visit(node, ctx);
83
}
84
85
private String extractMethodName(ASTDefnDef methodNode) {
86
return methodNode.descendants(ASTTermName.class)
87
.findFirst()
88
.map(name -> name.getText())
89
.orElse("unknown");
90
}
91
}
92
```
93
94
### Expression-Level Analysis Rules
95
96
```java
97
public class NestedBlockRule extends ScalaRule {
98
private static final int MAX_NESTING_DEPTH = 4;
99
100
@Override
101
public RuleContext visit(ASTTermBlock node, RuleContext ctx) {
102
int nestingDepth = calculateNestingDepth(node);
103
104
if (nestingDepth > MAX_NESTING_DEPTH) {
105
ctx.addViolation(node,
106
"Code block is nested too deeply: " + nestingDepth +
107
" levels (max: " + MAX_NESTING_DEPTH + ")");
108
}
109
110
return super.visit(node, ctx);
111
}
112
113
private int calculateNestingDepth(ASTTermBlock block) {
114
int depth = 0;
115
ScalaNode<?> current = block.getParent();
116
117
while (current != null) {
118
if (current instanceof ASTTermBlock ||
119
current instanceof ASTTermIf ||
120
current instanceof ASTTermMatch ||
121
current instanceof ASTTermFor) {
122
depth++;
123
}
124
current = current.getParent();
125
}
126
127
return depth;
128
}
129
}
130
```
131
132
### Pattern Matching Analysis Rules
133
134
```java
135
public class MatchExpressionRule extends ScalaRule {
136
private static final int MAX_CASES = 10;
137
138
@Override
139
public RuleContext visit(ASTTermMatch node, RuleContext ctx) {
140
// Count case clauses
141
long caseCount = node.descendants(ASTCase.class).count();
142
143
if (caseCount > MAX_CASES) {
144
ctx.addViolation(node,
145
"Match expression has too many cases: " + caseCount +
146
" (max: " + MAX_CASES + ")");
147
}
148
149
// Check for wildcard patterns
150
boolean hasWildcard = node.descendants(ASTPatWildcard.class).count() > 0;
151
if (!hasWildcard) {
152
ctx.addViolation(node,
153
"Match expression should include a wildcard case for safety");
154
}
155
156
return super.visit(node, ctx);
157
}
158
159
@Override
160
public RuleContext visit(ASTCase node, RuleContext ctx) {
161
// Analyze individual case patterns
162
analyzeCasePattern(node, ctx);
163
return super.visit(node, ctx);
164
}
165
166
private void analyzeCasePattern(ASTCase caseNode, RuleContext ctx) {
167
// Check for complex nested patterns
168
long patternDepth = caseNode.descendants(ASTPatExtract.class)
169
.mapToInt(this::getPatternDepth)
170
.max()
171
.orElse(0);
172
173
if (patternDepth > 3) {
174
ctx.addViolation(caseNode,
175
"Case pattern is too complex (depth: " + patternDepth + ")");
176
}
177
}
178
179
private int getPatternDepth(ASTPatExtract pattern) {
180
// Calculate nesting depth of pattern extractors
181
int depth = 1;
182
pattern.descendants(ASTPatExtract.class).forEach(nested -> depth++);
183
return depth;
184
}
185
}
186
```
187
188
## Import and Package Rules
189
190
### Import Organization Rules
191
192
```java
193
public class ImportOrganizationRule extends ScalaRule {
194
@Override
195
public RuleContext visit(ASTImport node, RuleContext ctx) {
196
// Check for wildcard imports
197
if (hasWildcardImport(node)) {
198
String importPath = extractImportPath(node);
199
if (!isAllowedWildcardImport(importPath)) {
200
ctx.addViolation(node,
201
"Avoid wildcard imports except for allowed packages: " + importPath);
202
}
203
}
204
205
// Check for unused imports (requires additional analysis)
206
checkUnusedImports(node, ctx);
207
208
return super.visit(node, ctx);
209
}
210
211
private boolean hasWildcardImport(ASTImport importNode) {
212
return importNode.descendants(ASTImporteeWildcard.class).count() > 0;
213
}
214
215
private boolean isAllowedWildcardImport(String importPath) {
216
return importPath.startsWith("scala.collection.") ||
217
importPath.startsWith("java.util.") ||
218
importPath.equals("scala.concurrent.ExecutionContext.Implicits");
219
}
220
221
private String extractImportPath(ASTImport importNode) {
222
return importNode.descendants(ASTImporter.class)
223
.findFirst()
224
.map(imp -> imp.getText())
225
.orElse("");
226
}
227
228
private void checkUnusedImports(ASTImport importNode, RuleContext ctx) {
229
// Complex analysis requiring symbol resolution
230
// Implementation would check if imported symbols are used in the file
231
}
232
}
233
```
234
235
### Package Naming Rules
236
237
```java
238
public class PackageNamingRule extends ScalaRule {
239
private static final Pattern PACKAGE_PATTERN =
240
Pattern.compile("^[a-z]+(\\.[a-z][a-z0-9]*)*$");
241
242
@Override
243
public RuleContext visit(ASTPkg node, RuleContext ctx) {
244
String packageName = extractPackageName(node);
245
246
if (!PACKAGE_PATTERN.matcher(packageName).matches()) {
247
ctx.addViolation(node,
248
"Package name '" + packageName +
249
"' should be lowercase with dots as separators");
250
}
251
252
return super.visit(node, ctx);
253
}
254
255
private String extractPackageName(ASTPkg packageNode) {
256
return packageNode.getText().replaceFirst("package\\s+", "").trim();
257
}
258
}
259
```
260
261
## Type and Generic Rules
262
263
### Generic Type Usage Rules
264
265
```java
266
public class GenericTypeRule extends ScalaRule {
267
@Override
268
public RuleContext visit(ASTTypeParam node, RuleContext ctx) {
269
String paramName = node.getText();
270
271
// Check type parameter naming convention
272
if (!paramName.matches("^[A-Z][A-Za-z0-9]*$")) {
273
ctx.addViolation(node,
274
"Type parameter '" + paramName +
275
"' should start with uppercase letter");
276
}
277
278
// Check for single-letter type parameters (often preferred)
279
if (paramName.length() > 2 && isSingleConceptType(paramName)) {
280
ctx.addViolation(node,
281
"Consider using single-letter type parameter: " + paramName);
282
}
283
284
return super.visit(node, ctx);
285
}
286
287
@Override
288
public RuleContext visit(ASTTypeApply node, RuleContext ctx) {
289
// Check for raw types (missing type parameters)
290
checkRawTypeUsage(node, ctx);
291
return super.visit(node, ctx);
292
}
293
294
private boolean isSingleConceptType(String paramName) {
295
return paramName.equals("Element") ||
296
paramName.equals("Value") ||
297
paramName.equals("Key");
298
}
299
300
private void checkRawTypeUsage(ASTTypeApply typeApp, RuleContext ctx) {
301
// Analysis of generic type applications
302
String typeName = extractTypeName(typeApp);
303
if (isGenericType(typeName) && !hasTypeArguments(typeApp)) {
304
ctx.addViolation(typeApp,
305
"Raw type usage detected: " + typeName +
306
" should specify type parameters");
307
}
308
}
309
310
private boolean isGenericType(String typeName) {
311
return typeName.equals("List") ||
312
typeName.equals("Map") ||
313
typeName.equals("Option") ||
314
typeName.equals("Future");
315
}
316
317
private boolean hasTypeArguments(ASTTypeApply typeApp) {
318
return typeApp.getNumChildren() > 1;
319
}
320
321
private String extractTypeName(ASTTypeApply typeApp) {
322
return typeApp.descendants(ASTTypeName.class)
323
.findFirst()
324
.map(name -> name.getText())
325
.orElse("");
326
}
327
}
328
```
329
330
## Rule Configuration and Properties
331
332
### Configurable Rule Properties
333
334
```java
335
public class ConfigurableComplexityRule extends ScalaRule {
336
private static final IntegerProperty MAX_METHODS =
337
IntegerProperty.named("maxMethods")
338
.desc("Maximum number of methods per class")
339
.defaultValue(15)
340
.range(5, 50)
341
.build();
342
343
private static final BooleanProperty IGNORE_PRIVATE =
344
BooleanProperty.named("ignorePrivateMethods")
345
.desc("Ignore private methods in count")
346
.defaultValue(false)
347
.build();
348
349
public ConfigurableComplexityRule() {
350
definePropertyDescriptor(MAX_METHODS);
351
definePropertyDescriptor(IGNORE_PRIVATE);
352
}
353
354
@Override
355
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
356
int maxMethods = getProperty(MAX_METHODS);
357
boolean ignorePrivate = getProperty(IGNORE_PRIVATE);
358
359
long methodCount = countMethods(node, ignorePrivate);
360
361
if (methodCount > maxMethods) {
362
ctx.addViolation(node,
363
"Class has " + methodCount + " methods (max: " + maxMethods + ")");
364
}
365
366
return super.visit(node, ctx);
367
}
368
369
private long countMethods(ASTDefnClass classNode, boolean ignorePrivate) {
370
Stream<ASTDefnDef> methods = classNode.descendants(ASTDefnDef.class);
371
372
if (ignorePrivate) {
373
methods = methods.filter(method -> !isPrivateMethod(method));
374
}
375
376
return methods.count();
377
}
378
379
private boolean isPrivateMethod(ASTDefnDef method) {
380
return method.descendants(ASTModPrivate.class).count() > 0;
381
}
382
}
383
```
384
385
## Advanced Rule Techniques
386
387
### Multi-Pass Analysis
388
389
```java
390
public class CrossReferenceRule extends ScalaRule {
391
private final Map<String, List<ASTDefnClass>> classRegistry = new HashMap<>();
392
private final Set<String> usedClasses = new HashSet<>();
393
394
@Override
395
public void start(RuleContext ctx) {
396
// First pass: collect all class definitions
397
super.start(ctx);
398
classRegistry.clear();
399
usedClasses.clear();
400
}
401
402
@Override
403
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
404
String className = extractClassName(node);
405
classRegistry.computeIfAbsent(className, k -> new ArrayList<>()).add(node);
406
return super.visit(node, ctx);
407
}
408
409
@Override
410
public RuleContext visit(ASTTermNew node, RuleContext ctx) {
411
// Track class usage in object instantiation
412
String usedClass = extractInstantiatedClass(node);
413
usedClasses.add(usedClass);
414
return super.visit(node, ctx);
415
}
416
417
@Override
418
public void end(RuleContext ctx) {
419
// Second pass: check for unused classes
420
classRegistry.forEach((className, instances) -> {
421
if (!usedClasses.contains(className) && !isExcluded(className)) {
422
instances.forEach(classNode ->
423
ctx.addViolation(classNode,
424
"Class '" + className + "' appears to be unused"));
425
}
426
});
427
428
super.end(ctx);
429
}
430
431
private boolean isExcluded(String className) {
432
return className.endsWith("Test") ||
433
className.endsWith("Spec") ||
434
className.equals("Main");
435
}
436
}
437
```
438
439
### Context-Aware Rules
440
441
```java
442
public class ContextAwareRule extends ScalaRule {
443
private final Deque<String> contextStack = new ArrayDeque<>();
444
445
@Override
446
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
447
String className = extractClassName(node);
448
contextStack.push("class:" + className);
449
450
RuleContext result = super.visit(node, ctx);
451
452
contextStack.pop();
453
return result;
454
}
455
456
@Override
457
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
458
String methodName = extractMethodName(node);
459
String currentContext = String.join("/", contextStack);
460
461
// Apply context-specific rules
462
if (currentContext.contains("Test")) {
463
checkTestMethodNaming(node, methodName, ctx);
464
} else {
465
checkRegularMethodNaming(node, methodName, ctx);
466
}
467
468
return super.visit(node, ctx);
469
}
470
471
private void checkTestMethodNaming(ASTDefnDef node, String methodName, RuleContext ctx) {
472
if (!methodName.startsWith("test") && !methodName.contains("should")) {
473
ctx.addViolation(node,
474
"Test method '" + methodName +
475
"' should start with 'test' or contain 'should'");
476
}
477
}
478
479
private void checkRegularMethodNaming(ASTDefnDef node, String methodName, RuleContext ctx) {
480
if (!methodName.matches("^[a-z][a-zA-Z0-9]*$")) {
481
ctx.addViolation(node,
482
"Method '" + methodName + "' should be camelCase");
483
}
484
}
485
}
486
```
487
488
## Integration with PMD Framework
489
490
### Rule Registration
491
492
Rules are registered with PMD through rule set XML files:
493
494
```xml
495
<?xml version="1.0"?>
496
<ruleset name="Custom Scala Rules">
497
<description>Custom static analysis rules for Scala code</description>
498
499
<rule name="ClassComplexity"
500
language="scala"
501
class="com.example.rules.ClassComplexityRule">
502
<description>Checks for classes with too many methods</description>
503
<properties>
504
<property name="maxMethods" value="15"/>
505
<property name="ignorePrivateMethods" value="false"/>
506
</properties>
507
</rule>
508
509
<rule name="MethodNaming"
510
language="scala"
511
class="com.example.rules.MethodNamingRule">
512
<description>Enforces camelCase method naming</description>
513
</rule>
514
</ruleset>
515
```
516
517
### Rule Testing
518
519
```java
520
// Unit test for rule validation
521
public class ClassComplexityRuleTest {
522
@Test
523
public void testClassWithTooManyMethods() {
524
String code = """
525
class LargeClass {
526
def method1() = {}
527
def method2() = {}
528
// ... more methods
529
def method20() = {}
530
}
531
""";
532
533
Rule rule = new ClassComplexityRule();
534
List<RuleViolation> violations = PMDTestUtils.check(rule, code);
535
536
assertEquals(1, violations.size());
537
assertTrue(violations.get(0).getDescription().contains("too many methods"));
538
}
539
}
540
```
541
542
This rule development framework provides comprehensive capabilities for creating sophisticated Scala static analysis rules that integrate seamlessly with PMD's analysis pipeline.