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.
Primary base class for developing Scala-specific PMD rules with integrated visitor pattern support.
public class ScalaRule extends AbstractRule implements ScalaVisitor<RuleContext, RuleContext> {
public void apply(Node target, RuleContext ctx);
public RuleContext visitNode(Node node, RuleContext param);
// Default visitor implementations for all node types
public RuleContext visit(ScalaNode<?> node, RuleContext data);
public RuleContext visit(ASTSource node, RuleContext data);
// Override specific visit methods for targeted analysis
public RuleContext visit(ASTDefnClass node, RuleContext data);
public RuleContext visit(ASTDefnDef node, RuleContext data);
public RuleContext visit(ASTTermApply node, RuleContext data);
// ... methods for all 140+ AST node types
}Basic Rule Implementation:
public class ClassComplexityRule extends ScalaRule {
private static final int MAX_METHODS = 15;
@Override
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
// Count methods in this class
long methodCount = node.descendants(ASTDefnDef.class).count();
if (methodCount > MAX_METHODS) {
ctx.addViolation(node,
"Class '" + getClassName(node) + "' has too many methods: " +
methodCount + " (max: " + MAX_METHODS + ")");
}
// Continue visiting child nodes
return super.visit(node, ctx);
}
private String getClassName(ASTDefnClass classNode) {
// Extract class name from AST node
return classNode.descendants(ASTTermName.class)
.findFirst()
.map(name -> name.getText())
.orElse("Unknown");
}
}public class MethodNamingRule extends ScalaRule {
private static final Pattern CAMEL_CASE = Pattern.compile("^[a-z][a-zA-Z0-9]*$");
@Override
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
String methodName = extractMethodName(node);
if (!CAMEL_CASE.matcher(methodName).matches()) {
ctx.addViolation(node,
"Method name '" + methodName + "' should be in camelCase");
}
// Check method length
int lineCount = node.getEndLine() - node.getBeginLine() + 1;
if (lineCount > 50) {
ctx.addViolation(node,
"Method '" + methodName + "' is too long: " + lineCount + " lines");
}
return super.visit(node, ctx);
}
private String extractMethodName(ASTDefnDef methodNode) {
return methodNode.descendants(ASTTermName.class)
.findFirst()
.map(name -> name.getText())
.orElse("unknown");
}
}public class NestedBlockRule extends ScalaRule {
private static final int MAX_NESTING_DEPTH = 4;
@Override
public RuleContext visit(ASTTermBlock node, RuleContext ctx) {
int nestingDepth = calculateNestingDepth(node);
if (nestingDepth > MAX_NESTING_DEPTH) {
ctx.addViolation(node,
"Code block is nested too deeply: " + nestingDepth +
" levels (max: " + MAX_NESTING_DEPTH + ")");
}
return super.visit(node, ctx);
}
private int calculateNestingDepth(ASTTermBlock block) {
int depth = 0;
ScalaNode<?> current = block.getParent();
while (current != null) {
if (current instanceof ASTTermBlock ||
current instanceof ASTTermIf ||
current instanceof ASTTermMatch ||
current instanceof ASTTermFor) {
depth++;
}
current = current.getParent();
}
return depth;
}
}public class MatchExpressionRule extends ScalaRule {
private static final int MAX_CASES = 10;
@Override
public RuleContext visit(ASTTermMatch node, RuleContext ctx) {
// Count case clauses
long caseCount = node.descendants(ASTCase.class).count();
if (caseCount > MAX_CASES) {
ctx.addViolation(node,
"Match expression has too many cases: " + caseCount +
" (max: " + MAX_CASES + ")");
}
// Check for wildcard patterns
boolean hasWildcard = node.descendants(ASTPatWildcard.class).count() > 0;
if (!hasWildcard) {
ctx.addViolation(node,
"Match expression should include a wildcard case for safety");
}
return super.visit(node, ctx);
}
@Override
public RuleContext visit(ASTCase node, RuleContext ctx) {
// Analyze individual case patterns
analyzeCasePattern(node, ctx);
return super.visit(node, ctx);
}
private void analyzeCasePattern(ASTCase caseNode, RuleContext ctx) {
// Check for complex nested patterns
long patternDepth = caseNode.descendants(ASTPatExtract.class)
.mapToInt(this::getPatternDepth)
.max()
.orElse(0);
if (patternDepth > 3) {
ctx.addViolation(caseNode,
"Case pattern is too complex (depth: " + patternDepth + ")");
}
}
private int getPatternDepth(ASTPatExtract pattern) {
// Calculate nesting depth of pattern extractors
int depth = 1;
pattern.descendants(ASTPatExtract.class).forEach(nested -> depth++);
return depth;
}
}public class ImportOrganizationRule extends ScalaRule {
@Override
public RuleContext visit(ASTImport node, RuleContext ctx) {
// Check for wildcard imports
if (hasWildcardImport(node)) {
String importPath = extractImportPath(node);
if (!isAllowedWildcardImport(importPath)) {
ctx.addViolation(node,
"Avoid wildcard imports except for allowed packages: " + importPath);
}
}
// Check for unused imports (requires additional analysis)
checkUnusedImports(node, ctx);
return super.visit(node, ctx);
}
private boolean hasWildcardImport(ASTImport importNode) {
return importNode.descendants(ASTImporteeWildcard.class).count() > 0;
}
private boolean isAllowedWildcardImport(String importPath) {
return importPath.startsWith("scala.collection.") ||
importPath.startsWith("java.util.") ||
importPath.equals("scala.concurrent.ExecutionContext.Implicits");
}
private String extractImportPath(ASTImport importNode) {
return importNode.descendants(ASTImporter.class)
.findFirst()
.map(imp -> imp.getText())
.orElse("");
}
private void checkUnusedImports(ASTImport importNode, RuleContext ctx) {
// Complex analysis requiring symbol resolution
// Implementation would check if imported symbols are used in the file
}
}public class PackageNamingRule extends ScalaRule {
private static final Pattern PACKAGE_PATTERN =
Pattern.compile("^[a-z]+(\\.[a-z][a-z0-9]*)*$");
@Override
public RuleContext visit(ASTPkg node, RuleContext ctx) {
String packageName = extractPackageName(node);
if (!PACKAGE_PATTERN.matcher(packageName).matches()) {
ctx.addViolation(node,
"Package name '" + packageName +
"' should be lowercase with dots as separators");
}
return super.visit(node, ctx);
}
private String extractPackageName(ASTPkg packageNode) {
return packageNode.getText().replaceFirst("package\\s+", "").trim();
}
}public class GenericTypeRule extends ScalaRule {
@Override
public RuleContext visit(ASTTypeParam node, RuleContext ctx) {
String paramName = node.getText();
// Check type parameter naming convention
if (!paramName.matches("^[A-Z][A-Za-z0-9]*$")) {
ctx.addViolation(node,
"Type parameter '" + paramName +
"' should start with uppercase letter");
}
// Check for single-letter type parameters (often preferred)
if (paramName.length() > 2 && isSingleConceptType(paramName)) {
ctx.addViolation(node,
"Consider using single-letter type parameter: " + paramName);
}
return super.visit(node, ctx);
}
@Override
public RuleContext visit(ASTTypeApply node, RuleContext ctx) {
// Check for raw types (missing type parameters)
checkRawTypeUsage(node, ctx);
return super.visit(node, ctx);
}
private boolean isSingleConceptType(String paramName) {
return paramName.equals("Element") ||
paramName.equals("Value") ||
paramName.equals("Key");
}
private void checkRawTypeUsage(ASTTypeApply typeApp, RuleContext ctx) {
// Analysis of generic type applications
String typeName = extractTypeName(typeApp);
if (isGenericType(typeName) && !hasTypeArguments(typeApp)) {
ctx.addViolation(typeApp,
"Raw type usage detected: " + typeName +
" should specify type parameters");
}
}
private boolean isGenericType(String typeName) {
return typeName.equals("List") ||
typeName.equals("Map") ||
typeName.equals("Option") ||
typeName.equals("Future");
}
private boolean hasTypeArguments(ASTTypeApply typeApp) {
return typeApp.getNumChildren() > 1;
}
private String extractTypeName(ASTTypeApply typeApp) {
return typeApp.descendants(ASTTypeName.class)
.findFirst()
.map(name -> name.getText())
.orElse("");
}
}public class ConfigurableComplexityRule extends ScalaRule {
private static final IntegerProperty MAX_METHODS =
IntegerProperty.named("maxMethods")
.desc("Maximum number of methods per class")
.defaultValue(15)
.range(5, 50)
.build();
private static final BooleanProperty IGNORE_PRIVATE =
BooleanProperty.named("ignorePrivateMethods")
.desc("Ignore private methods in count")
.defaultValue(false)
.build();
public ConfigurableComplexityRule() {
definePropertyDescriptor(MAX_METHODS);
definePropertyDescriptor(IGNORE_PRIVATE);
}
@Override
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
int maxMethods = getProperty(MAX_METHODS);
boolean ignorePrivate = getProperty(IGNORE_PRIVATE);
long methodCount = countMethods(node, ignorePrivate);
if (methodCount > maxMethods) {
ctx.addViolation(node,
"Class has " + methodCount + " methods (max: " + maxMethods + ")");
}
return super.visit(node, ctx);
}
private long countMethods(ASTDefnClass classNode, boolean ignorePrivate) {
Stream<ASTDefnDef> methods = classNode.descendants(ASTDefnDef.class);
if (ignorePrivate) {
methods = methods.filter(method -> !isPrivateMethod(method));
}
return methods.count();
}
private boolean isPrivateMethod(ASTDefnDef method) {
return method.descendants(ASTModPrivate.class).count() > 0;
}
}public class CrossReferenceRule extends ScalaRule {
private final Map<String, List<ASTDefnClass>> classRegistry = new HashMap<>();
private final Set<String> usedClasses = new HashSet<>();
@Override
public void start(RuleContext ctx) {
// First pass: collect all class definitions
super.start(ctx);
classRegistry.clear();
usedClasses.clear();
}
@Override
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
String className = extractClassName(node);
classRegistry.computeIfAbsent(className, k -> new ArrayList<>()).add(node);
return super.visit(node, ctx);
}
@Override
public RuleContext visit(ASTTermNew node, RuleContext ctx) {
// Track class usage in object instantiation
String usedClass = extractInstantiatedClass(node);
usedClasses.add(usedClass);
return super.visit(node, ctx);
}
@Override
public void end(RuleContext ctx) {
// Second pass: check for unused classes
classRegistry.forEach((className, instances) -> {
if (!usedClasses.contains(className) && !isExcluded(className)) {
instances.forEach(classNode ->
ctx.addViolation(classNode,
"Class '" + className + "' appears to be unused"));
}
});
super.end(ctx);
}
private boolean isExcluded(String className) {
return className.endsWith("Test") ||
className.endsWith("Spec") ||
className.equals("Main");
}
}public class ContextAwareRule extends ScalaRule {
private final Deque<String> contextStack = new ArrayDeque<>();
@Override
public RuleContext visit(ASTDefnClass node, RuleContext ctx) {
String className = extractClassName(node);
contextStack.push("class:" + className);
RuleContext result = super.visit(node, ctx);
contextStack.pop();
return result;
}
@Override
public RuleContext visit(ASTDefnDef node, RuleContext ctx) {
String methodName = extractMethodName(node);
String currentContext = String.join("/", contextStack);
// Apply context-specific rules
if (currentContext.contains("Test")) {
checkTestMethodNaming(node, methodName, ctx);
} else {
checkRegularMethodNaming(node, methodName, ctx);
}
return super.visit(node, ctx);
}
private void checkTestMethodNaming(ASTDefnDef node, String methodName, RuleContext ctx) {
if (!methodName.startsWith("test") && !methodName.contains("should")) {
ctx.addViolation(node,
"Test method '" + methodName +
"' should start with 'test' or contain 'should'");
}
}
private void checkRegularMethodNaming(ASTDefnDef node, String methodName, RuleContext ctx) {
if (!methodName.matches("^[a-z][a-zA-Z0-9]*$")) {
ctx.addViolation(node,
"Method '" + methodName + "' should be camelCase");
}
}
}Rules are registered with PMD through rule set XML files:
<?xml version="1.0"?>
<ruleset name="Custom Scala Rules">
<description>Custom static analysis rules for Scala code</description>
<rule name="ClassComplexity"
language="scala"
class="com.example.rules.ClassComplexityRule">
<description>Checks for classes with too many methods</description>
<properties>
<property name="maxMethods" value="15"/>
<property name="ignorePrivateMethods" value="false"/>
</properties>
</rule>
<rule name="MethodNaming"
language="scala"
class="com.example.rules.MethodNamingRule">
<description>Enforces camelCase method naming</description>
</rule>
</ruleset>// Unit test for rule validation
public class ClassComplexityRuleTest {
@Test
public void testClassWithTooManyMethods() {
String code = """
class LargeClass {
def method1() = {}
def method2() = {}
// ... more methods
def method20() = {}
}
""";
Rule rule = new ClassComplexityRule();
List<RuleViolation> violations = PMDTestUtils.check(rule, code);
assertEquals(1, violations.size());
assertTrue(violations.get(0).getDescription().contains("too many methods"));
}
}This rule development framework provides comprehensive capabilities for creating sophisticated Scala static analysis rules that integrate seamlessly with PMD's analysis pipeline.