or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

ast-parsing.mdast-traversal.mdcopy-paste-detection.mdindex.mdlanguage-module.mdrule-development.md
tile.json

rule-development.mddocs/

Rule Development

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.

Core Rule Development Components

ScalaRule Base Class

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");
    }
}

Rule Implementation Patterns

Method-Level Analysis Rules

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");
    }
}

Expression-Level Analysis Rules

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;
    }
}

Pattern Matching Analysis Rules

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;
    }
}

Import and Package Rules

Import Organization Rules

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
    }
}

Package Naming Rules

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();
    }
}

Type and Generic Rules

Generic Type Usage Rules

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("");
    }
}

Rule Configuration and Properties

Configurable Rule Properties

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;
    }
}

Advanced Rule Techniques

Multi-Pass Analysis

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");
    }
}

Context-Aware Rules

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");
        }
    }
}

Integration with PMD Framework

Rule Registration

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>

Rule Testing

// 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.