PMD Scala language support for Scala 2.12 - provides parsing and static analysis capabilities for Scala code as part of the PMD extensible multilanguage static code analyzer
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.
Install with Tessl CLI
npx tessl i tessl/maven-net-sourceforge-pmd--pmd-scala-2-12