0
# Rule Development Framework
1
2
Base classes and infrastructure for developing custom PMD rules for Scala code analysis, including rule violation factories and chain visitors for efficient processing. This framework enables creation of static analysis rules specifically tailored for Scala language constructs.
3
4
## Capabilities
5
6
### Base Scala Rule Class
7
8
Foundation class for implementing custom PMD rules for Scala code analysis.
9
10
```java { .api }
11
/**
12
* The default base implementation of a PMD Rule for Scala. Uses the Visitor
13
* Pattern to traverse the AST.
14
*/
15
public class ScalaRule extends AbstractRule implements ScalaParserVisitor<RuleContext, RuleContext> {
16
/**
17
* Create a new Scala Rule
18
* Automatically registers with Scala language module
19
*/
20
public ScalaRule();
21
22
/**
23
* Apply this rule to a collection of AST nodes
24
* @param nodes the nodes to analyze
25
* @param ctx the rule context for reporting violations
26
*/
27
@Override
28
public void apply(List<? extends Node> nodes, RuleContext ctx);
29
30
/**
31
* Generic visit method for any Scala node
32
* Default implementation visits all children recursively
33
* @param node the node to visit
34
* @param data the rule context
35
* @return the rule context
36
*/
37
@Override
38
public RuleContext visit(ScalaNode<?> node, RuleContext data);
39
40
/**
41
* Visit the root source node
42
* @param node the source root node
43
* @param data the rule context
44
* @return the rule context
45
*/
46
@Override
47
public RuleContext visit(ASTSource node, RuleContext data);
48
49
// All ScalaParserVisitor methods are implemented with default delegation
50
// Override specific methods to implement rule logic for particular node types
51
52
@Override
53
public RuleContext visit(ASTDefnClass node, RuleContext data);
54
@Override
55
public RuleContext visit(ASTDefnDef node, RuleContext data);
56
@Override
57
public RuleContext visit(ASTTermApply node, RuleContext data);
58
// ... and 100+ other visit methods for all AST node types
59
}
60
```
61
62
**Usage Examples:**
63
64
```java
65
// Simple rule checking for long method names
66
public class LongMethodNameRule extends ScalaRule {
67
private static final int MAX_METHOD_NAME_LENGTH = 30;
68
69
@Override
70
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
71
// Extract method name from the AST node
72
String methodName = extractMethodName(node);
73
74
if (methodName.length() > MAX_METHOD_NAME_LENGTH) {
75
addViolation(ctx, node,
76
"Method name '" + methodName + "' is too long (" +
77
methodName.length() + " characters, max " + MAX_METHOD_NAME_LENGTH + ")");
78
}
79
80
// Continue visiting children
81
return super.visit(node, ctx);
82
}
83
84
private String extractMethodName(ASTDefnDef methodNode) {
85
// Implementation to extract method name from AST
86
// This would traverse the node structure to find the name
87
return "extractedName"; // Simplified
88
}
89
}
90
91
// Rule checking for unused imports
92
public class UnusedImportRule extends ScalaRule {
93
private Set<String> importedNames = new HashSet<>();
94
private Set<String> usedNames = new HashSet<>();
95
96
@Override
97
public RuleContext visit(ASTSource node, RuleContext ctx) {
98
// Reset for each source file
99
importedNames.clear();
100
usedNames.clear();
101
102
// Visit entire tree
103
RuleContext result = super.visit(node, ctx);
104
105
// Report unused imports
106
for (String importedName : importedNames) {
107
if (!usedNames.contains(importedName)) {
108
addViolation(ctx, node, "Unused import: " + importedName);
109
}
110
}
111
112
return result;
113
}
114
115
@Override
116
public RuleContext visit(ASTImport node, RuleContext ctx) {
117
// Collect imported names
118
String importName = extractImportName(node);
119
importedNames.add(importName);
120
121
return super.visit(node, ctx);
122
}
123
124
@Override
125
public RuleContext visit(ASTTermName node, RuleContext ctx) {
126
// Track name usage
127
String name = extractTermName(node);
128
usedNames.add(name);
129
130
return super.visit(node, ctx);
131
}
132
133
private String extractImportName(ASTImport importNode) {
134
// Extract import name from AST structure
135
return "importedName"; // Simplified
136
}
137
138
private String extractTermName(ASTTermName nameNode) {
139
// Extract term name from AST structure
140
return "termName"; // Simplified
141
}
142
}
143
```
144
145
### Rule Violation Factory
146
147
Factory for creating rule violations with proper Scala-specific formatting and positioning.
148
149
```java { .api }
150
/**
151
* Factory for creating rule violations for Scala rules
152
*/
153
public class ScalaRuleViolationFactory implements RuleViolationFactory {
154
/**
155
* Singleton instance of the factory
156
*/
157
public static final ScalaRuleViolationFactory INSTANCE;
158
159
/**
160
* Create a rule violation for a Scala AST node
161
* @param rule the rule that was violated
162
* @param ctx the rule context
163
* @param node the AST node where violation occurred
164
* @param message the violation message
165
* @return created RuleViolation instance
166
*/
167
@Override
168
public RuleViolation createRuleViolation(Rule rule, RuleContext ctx, Node node, String message);
169
170
/**
171
* Create a rule violation with additional details
172
* @param rule the rule that was violated
173
* @param ctx the rule context
174
* @param node the AST node where violation occurred
175
* @param message the violation message
176
* @param beginLine beginning line number
177
* @param endLine ending line number
178
* @return created RuleViolation instance
179
*/
180
@Override
181
public RuleViolation createRuleViolation(Rule rule, RuleContext ctx, Node node, String message,
182
int beginLine, int endLine);
183
}
184
```
185
186
**Usage Examples:**
187
188
```java
189
public class CustomScalaRule extends ScalaRule {
190
@Override
191
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
192
if (violatesRule(node)) {
193
// Use the factory to create properly formatted violations
194
RuleViolation violation = ScalaRuleViolationFactory.INSTANCE
195
.createRuleViolation(this, ctx, node, "Custom rule violation message");
196
197
ctx.getReport().addRuleViolation(violation);
198
}
199
200
return super.visit(node, ctx);
201
}
202
203
private boolean violatesRule(ASTDefnClass classNode) {
204
// Custom rule logic
205
return true;
206
}
207
}
208
```
209
210
### Rule Chain Visitor
211
212
Optimized visitor for processing multiple rules efficiently in a single AST traversal.
213
214
```java { .api }
215
/**
216
* Rule chain visitor for efficient rule processing
217
* Allows multiple rules to be applied in a single AST traversal
218
*/
219
public class ScalaRuleChainVisitor implements ScalaParserVisitor<RuleContext, RuleContext> {
220
/**
221
* Create rule chain visitor with collection of rules
222
* @param rules the rules to apply during traversal
223
*/
224
public ScalaRuleChainVisitor(Collection<ScalaRule> rules);
225
226
/**
227
* Add a rule to the chain
228
* @param rule the rule to add
229
*/
230
public void addRule(ScalaRule rule);
231
232
/**
233
* Remove a rule from the chain
234
* @param rule the rule to remove
235
*/
236
public void removeRule(ScalaRule rule);
237
238
/**
239
* Visit node with all rules in the chain
240
* @param node the node to visit
241
* @param ctx the rule context
242
* @return the rule context
243
*/
244
@Override
245
public RuleContext visit(ScalaNode<?> node, RuleContext ctx);
246
247
// Implements all visitor methods to apply rule chain
248
}
249
```
250
251
**Usage Examples:**
252
253
```java
254
// Create multiple rules
255
ScalaRule rule1 = new LongMethodNameRule();
256
ScalaRule rule2 = new UnusedImportRule();
257
ScalaRule rule3 = new ComplexityRule();
258
259
// Create chain visitor for efficient processing
260
Collection<ScalaRule> rules = Arrays.asList(rule1, rule2, rule3);
261
ScalaRuleChainVisitor chainVisitor = new ScalaRuleChainVisitor(rules);
262
263
// Apply all rules in single traversal
264
ASTSource ast = parser.parse("Example.scala", sourceReader);
265
RuleContext ctx = new RuleContext();
266
ast.accept(chainVisitor, ctx);
267
268
// All violations from all rules are now in the context
269
```
270
271
### Advanced Rule Patterns
272
273
Common patterns for implementing sophisticated Scala rules.
274
275
**Pattern 1: Multi-Pass Analysis**
276
277
```java
278
public class ComplexAnalysisRule extends ScalaRule {
279
private Map<String, ASTDefnClass> classes = new HashMap<>();
280
private Map<String, List<ASTDefnDef>> classMethods = new HashMap<>();
281
282
@Override
283
public RuleContext visit(ASTSource node, RuleContext ctx) {
284
// First pass: collect all classes and methods
285
collectClassesAndMethods(node);
286
287
// Second pass: analyze relationships
288
analyzeClassRelationships(ctx);
289
290
return super.visit(node, ctx);
291
}
292
293
private void collectClassesAndMethods(ASTSource root) {
294
root.accept(new ScalaParserVisitorAdapter() {
295
@Override
296
public Object visit(ASTDefnClass node, Object data) {
297
String className = extractClassName(node);
298
classes.put(className, node);
299
classMethods.put(className, new ArrayList<>());
300
return super.visit(node, data);
301
}
302
303
@Override
304
public Object visit(ASTDefnDef node, Object data) {
305
String currentClass = getCurrentClass(node);
306
if (currentClass != null) {
307
classMethods.get(currentClass).add(node);
308
}
309
return super.visit(node, data);
310
}
311
}, null);
312
}
313
314
private void analyzeClassRelationships(RuleContext ctx) {
315
// Complex analysis using collected data
316
for (Map.Entry<String, ASTDefnClass> entry : classes.entrySet()) {
317
String className = entry.getKey();
318
ASTDefnClass classNode = entry.getValue();
319
List<ASTDefnDef> methods = classMethods.get(className);
320
321
if (methods.size() > 20) {
322
addViolation(ctx, classNode,
323
"Class '" + className + "' has too many methods (" + methods.size() + ")");
324
}
325
}
326
}
327
328
private String extractClassName(ASTDefnClass node) { return "ClassName"; }
329
private String getCurrentClass(ASTDefnDef node) { return "CurrentClass"; }
330
}
331
```
332
333
**Pattern 2: Context-Aware Rules**
334
335
```java
336
public class ContextAwareRule extends ScalaRule {
337
private Stack<String> contextStack = new Stack<>();
338
339
@Override
340
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
341
String className = extractClassName(node);
342
contextStack.push("class:" + className);
343
344
try {
345
return super.visit(node, ctx);
346
} finally {
347
contextStack.pop();
348
}
349
}
350
351
@Override
352
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
353
String methodName = extractMethodName(node);
354
contextStack.push("method:" + methodName);
355
356
try {
357
// Rule logic can access full context
358
String currentContext = String.join(" -> ", contextStack);
359
360
if (violatesRule(node, currentContext)) {
361
addViolation(ctx, node, "Violation in context: " + currentContext);
362
}
363
364
return super.visit(node, ctx);
365
} finally {
366
contextStack.pop();
367
}
368
}
369
370
private boolean violatesRule(ASTDefnDef node, String context) {
371
// Context-sensitive rule logic
372
return context.contains("class:TestClass") &&
373
extractMethodName(node).startsWith("test");
374
}
375
376
private String extractClassName(ASTDefnClass node) { return "ClassName"; }
377
private String extractMethodName(ASTDefnDef node) { return "methodName"; }
378
}
379
```
380
381
**Pattern 3: Configuration-Based Rules**
382
383
```java
384
public class ConfigurableRule extends ScalaRule {
385
private int maxComplexity = 10;
386
private boolean checkPrivateMethods = false;
387
private Set<String> excludedPatterns = new HashSet<>();
388
389
public ConfigurableRule() {
390
// Load configuration from rule properties
391
loadConfiguration();
392
}
393
394
@Override
395
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
396
String methodName = extractMethodName(node);
397
398
// Skip excluded patterns
399
if (excludedPatterns.stream().anyMatch(methodName::matches)) {
400
return super.visit(node, ctx);
401
}
402
403
// Skip private methods if not configured to check them
404
if (!checkPrivateMethods && isPrivate(node)) {
405
return super.visit(node, ctx);
406
}
407
408
int complexity = calculateComplexity(node);
409
if (complexity > maxComplexity) {
410
addViolation(ctx, node,
411
"Method complexity (" + complexity + ") exceeds maximum (" + maxComplexity + ")");
412
}
413
414
return super.visit(node, ctx);
415
}
416
417
private void loadConfiguration() {
418
// Load from rule properties
419
maxComplexity = getIntProperty("maxComplexity", 10);
420
checkPrivateMethods = getBooleanProperty("checkPrivateMethods", false);
421
String excludePattern = getStringProperty("excludePatterns", "");
422
if (!excludePattern.isEmpty()) {
423
excludedPatterns.addAll(Arrays.asList(excludePattern.split(",")));
424
}
425
}
426
427
private String extractMethodName(ASTDefnDef node) { return "methodName"; }
428
private boolean isPrivate(ASTDefnDef node) { return false; }
429
private int calculateComplexity(ASTDefnDef node) { return 5; }
430
}
431
```
432
433
### Testing Scala Rules
434
435
Framework support for testing custom rules:
436
437
```java
438
// Example test class for Scala rules
439
public class ScalaRuleTest {
440
@Test
441
public void testLongMethodNameRule() {
442
ScalaRule rule = new LongMethodNameRule();
443
444
String scalaCode = """
445
object TestObject {
446
def shortName(): Unit = {}
447
def thisIsAVeryLongMethodNameThatExceedsTheLimit(): Unit = {}
448
}
449
""";
450
451
// Parse and apply rule
452
ASTSource ast = parseScala(scalaCode);
453
RuleContext ctx = new RuleContext();
454
ast.accept((ScalaParserVisitor<RuleContext, RuleContext>) rule, ctx);
455
456
// Verify violations
457
List<RuleViolation> violations = ctx.getReport().getViolations();
458
assertEquals(1, violations.size());
459
assertTrue(violations.get(0).getDescription().contains("too long"));
460
}
461
462
private ASTSource parseScala(String code) {
463
// Helper method to parse Scala code for testing
464
// Implementation would use ScalaParser
465
return null; // Simplified
466
}
467
}
468
```