PMD language module for static analysis of Apache Velocity Template Language (VTL) files
—
Base classes and utilities for creating custom static analysis rules for Velocity templates. The rule framework provides visitor pattern integration, violation reporting, and statistical analysis capabilities for developing both simple and complex rules.
Abstract base class that all Velocity rules must extend, providing visitor pattern integration and violation reporting.
/**
* Abstract base class for all Velocity template rules.
* Integrates with PMD's rule framework and provides visitor pattern support
* for analyzing Velocity AST nodes.
*/
public abstract class AbstractVmRule extends AbstractRule
implements VmParserVisitor, ImmutableLanguage {
/**
* Main entry point for rule execution.
* Called by PMD framework with list of AST nodes to analyze.
* @param nodes List of root nodes to analyze (typically one per template)
* @param ctx Rule context containing configuration and violation reporting
*/
public void apply(List<? extends Node> nodes, RuleContext ctx);
/**
* Helper method to visit all nodes in a list with proper context.
* Useful for processing multiple root nodes or child node collections.
* @param nodes List of Node instances to visit (filtered to VmNode internally)
* @param ctx Rule context for violation reporting
*/
protected void visitAll(List<? extends Node> nodes, RuleContext ctx);
// All VmParserVisitor methods are available for override:
/**
* Visit any VmNode. Default implementation traverses children.
* Override to provide custom handling for specific node types.
* @param node The AST node being visited
* @param data Rule context passed during traversal
* @return Typically null, or custom data for specialized rules
*/
public Object visit(VmNode node, Object data);
/**
* Visit variable reference nodes ($variable, $object.property).
* Override to analyze variable usage patterns.
* @param node Reference AST node
* @param data Rule context
* @return Typically null
*/
public Object visit(ASTReference node, Object data);
/**
* Visit directive nodes (#if, #foreach, #set, etc.).
* Override to analyze directive usage and structure.
* @param node Directive AST node
* @param data Rule context
* @return Typically null
*/
public Object visit(ASTDirective node, Object data);
/**
* Visit method call nodes.
* Override to analyze method invocation patterns.
* @param node Method call AST node
* @param data Rule context
* @return Typically null
*/
public Object visit(ASTMethod node, Object data);
// Mathematical operation visitor methods
public Object visit(ASTAddNode node, Object data);
public Object visit(ASTSubtractNode node, Object data);
public Object visit(ASTMulNode node, Object data);
public Object visit(ASTDivNode node, Object data);
public Object visit(ASTModNode node, Object data);
public Object visit(ASTMathNode node, Object data);
// Logical operation visitor methods
public Object visit(ASTOrNode node, Object data);
public Object visit(ASTAndNode node, Object data);
public Object visit(ASTNotNode node, Object data);
// Comparison operation visitor methods
public Object visit(ASTEQNode node, Object data);
public Object visit(ASTNENode node, Object data);
public Object visit(ASTLTNode node, Object data);
public Object visit(ASTGTNode node, Object data);
public Object visit(ASTLENode node, Object data);
public Object visit(ASTGENode node, Object data);
// Control flow visitor methods
public Object visit(ASTIfStatement node, Object data);
public Object visit(ASTElseStatement node, Object data);
public Object visit(ASTElseIfStatement node, Object data);
public Object visit(ASTForeachStatement node, Object data);
public Object visit(ASTSetDirective node, Object data);
public Object visit(ASTBlock node, Object data);
// Expression and structure visitor methods
public Object visit(ASTExpression node, Object data);
public Object visit(ASTAssignment node, Object data);
public Object visit(ASTMap node, Object data);
public Object visit(ASTObjectArray node, Object data);
public Object visit(ASTIntegerRange node, Object data);
public Object visit(ASTIndex node, Object data);
// Content visitor methods
public Object visit(ASTText node, Object data);
public Object visit(ASTTextblock node, Object data);
public Object visit(ASTComment node, Object data);
// Literal visitor methods
public Object visit(ASTFloatingPointLiteral node, Object data);
public Object visit(ASTIntegerLiteral node, Object data);
public Object visit(ASTStringLiteral node, Object data);
public Object visit(ASTTrue node, Object data);
public Object visit(ASTFalse node, Object data);
// Identifier visitor methods
public Object visit(ASTIdentifier node, Object data);
public Object visit(ASTWord node, Object data);
// Special node visitor methods
public Object visit(ASTEscape node, Object data);
public Object visit(ASTEscapedDirective node, Object data);
public Object visit(ASTprocess node, Object data);
}Specialized base class for rules that perform statistical analysis (complexity, length, nesting depth, etc.).
/**
* Abstract base class for statistical rules that measure quantitative
* properties of Velocity templates (length, complexity, nesting depth, etc.).
* Extends AbstractVmRule with statistical analysis capabilities.
*/
public abstract class AbstractStatisticalVmRule extends AbstractVmRule {
// Statistical analysis framework
// Subclasses implement specific metrics and thresholds
// Automatic threshold checking and violation reporting
/**
* Override to define the statistical metric being measured.
* Called by framework to collect measurement data.
* @param node AST node to measure
* @param data Context data
* @return Numeric measurement value
*/
protected abstract double getMeasurement(VmNode node, Object data);
/**
* Override to define the threshold for violations.
* Values exceeding this threshold trigger rule violations.
* @return Maximum acceptable value for the metric
*/
protected abstract double getThreshold();
/**
* Override to provide custom violation messages.
* @param measurement Actual measured value
* @param threshold Maximum acceptable value
* @return Descriptive violation message
*/
protected String getViolationMessage(double measurement, double threshold);
}import net.sourceforge.pmd.lang.vm.rule.AbstractVmRule;
import net.sourceforge.pmd.lang.vm.ast.ASTReference;
/**
* Example rule that flags references with problematic naming patterns.
*/
public class ProblematicReferenceRule extends AbstractVmRule {
@Override
public Object visit(ASTReference node, Object data) {
String refName = node.getRootString();
if (refName != null && isProblematic(refName)) {
addViolation(data, node,
"Reference name '" + refName + "' follows problematic pattern");
}
return super.visit(node, data);
}
private boolean isProblematic(String refName) {
// Example: flag references starting with underscore
return refName.startsWith("_") || refName.length() > 50;
}
}import net.sourceforge.pmd.lang.vm.rule.AbstractVmRule;
import net.sourceforge.pmd.lang.vm.ast.ASTIfStatement;
import net.sourceforge.pmd.lang.vm.ast.ASTForeachStatement;
import java.util.Stack;
/**
* Example rule that detects overly complex nested control structures.
*/
public class ComplexControlFlowRule extends AbstractVmRule {
private Stack<String> controlStack = new Stack<>();
private static final int MAX_NESTING = 3;
@Override
public Object visit(ASTIfStatement node, Object data) {
controlStack.push("if");
if (controlStack.size() > MAX_NESTING) {
addViolation(data, node,
"Control flow nesting exceeds maximum depth of " + MAX_NESTING);
}
Object result = super.visit(node, data);
controlStack.pop();
return result;
}
@Override
public Object visit(ASTForeachStatement node, Object data) {
controlStack.push("foreach");
if (controlStack.size() > MAX_NESTING) {
addViolation(data, node,
"Control flow nesting exceeds maximum depth of " + MAX_NESTING);
}
Object result = super.visit(node, data);
controlStack.pop();
return result;
}
}import net.sourceforge.pmd.lang.vm.rule.AbstractStatisticalVmRule;
import net.sourceforge.pmd.lang.vm.ast.VmNode;
import net.sourceforge.pmd.lang.vm.ast.ASTReference;
/**
* Example statistical rule that measures reference density in templates.
*/
public class ReferenceDensityRule extends AbstractStatisticalVmRule {
@Override
protected double getMeasurement(VmNode node, Object data) {
ReferenceCounter counter = new ReferenceCounter();
node.jjtAccept(counter, null);
int totalNodes = counter.getTotalNodes();
int referenceNodes = counter.getReferenceCount();
return totalNodes > 0 ? (double) referenceNodes / totalNodes : 0.0;
}
@Override
protected double getThreshold() {
return 0.5; // 50% reference density threshold
}
@Override
protected String getViolationMessage(double measurement, double threshold) {
return String.format("Reference density %.2f exceeds threshold %.2f",
measurement, threshold);
}
private static class ReferenceCounter extends VmParserVisitorAdapter {
private int totalNodes = 0;
private int referenceCount = 0;
@Override
public Object visit(VmNode node, Object data) {
totalNodes++;
return super.visit(node, data);
}
@Override
public Object visit(ASTReference node, Object data) {
referenceCount++;
return super.visit(node, data);
}
public int getTotalNodes() { return totalNodes; }
public int getReferenceCount() { return referenceCount; }
}
}import net.sourceforge.pmd.lang.vm.rule.AbstractVmRule;
import net.sourceforge.pmd.lang.vm.ast.ASTReference;
import net.sourceforge.pmd.lang.vm.ast.ASTSetDirective;
import java.util.Set;
import java.util.HashSet;
/**
* Example rule that tracks variable assignments and usage across the template.
*/
public class UnusedVariableRule extends AbstractVmRule {
private Set<String> definedVars = new HashSet<>();
private Set<String> usedVars = new HashSet<>();
@Override
public void apply(List<? extends Node> nodes, RuleContext ctx) {
// Reset state for each template
definedVars.clear();
usedVars.clear();
// First pass: collect definitions and usage
super.apply(nodes, ctx);
// Second pass: report unused variables
Set<String> unusedVars = new HashSet<>(definedVars);
unusedVars.removeAll(usedVars);
for (String unused : unusedVars) {
// Find the node where variable was defined for violation reporting
// This requires additional tracking during first pass
reportUnusedVariable(unused, ctx);
}
}
@Override
public Object visit(ASTSetDirective node, Object data) {
// Track variable definitions
String varName = extractVariableName(node);
if (varName != null) {
definedVars.add(varName);
}
return super.visit(node, data);
}
@Override
public Object visit(ASTReference node, Object data) {
// Track variable usage
String refName = node.getRootString();
if (refName != null) {
usedVars.add(refName);
}
return super.visit(node, data);
}
private String extractVariableName(ASTSetDirective node) {
// Implementation depends on AST structure
// Extract variable name from #set directive
return null; // Simplified for example
}
private void reportUnusedVariable(String varName, RuleContext ctx) {
// Implementation requires tracking definition locations
// Simplified for example
}
}import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
public class ConfigurableRule extends AbstractVmRule {
private static final PropertyDescriptor<Integer> MAX_LENGTH_PROP =
PropertyFactory.intProperty("maxLength")
.desc("Maximum allowed length")
.defaultValue(100)
.range(1, 1000)
.build();
private static final PropertyDescriptor<String> PATTERN_PROP =
PropertyFactory.stringProperty("pattern")
.desc("Regular expression pattern to match")
.defaultValue(".*")
.build();
public ConfigurableRule() {
definePropertyDescriptor(MAX_LENGTH_PROP);
definePropertyDescriptor(PATTERN_PROP);
}
@Override
public Object visit(ASTReference node, Object data) {
int maxLength = getProperty(MAX_LENGTH_PROP);
String pattern = getProperty(PATTERN_PROP);
String refName = node.getRootString();
if (refName != null) {
if (refName.length() > maxLength) {
addViolation(data, node, "Reference too long: " + refName.length());
}
if (!refName.matches(pattern)) {
addViolation(data, node, "Reference doesn't match pattern: " + refName);
}
}
return super.visit(node, data);
}
}public class EfficientRule extends AbstractVmRule {
// Pre-compile patterns and expensive objects
private static final Pattern PROBLEM_PATTERN = Pattern.compile("^temp_.*");
// Use early returns to avoid unnecessary processing
@Override
public Object visit(ASTReference node, Object data) {
String refName = node.getRootString();
if (refName == null) {
return super.visit(node, data); // Early return
}
if (PROBLEM_PATTERN.matcher(refName).matches()) {
addViolation(data, node, "Problematic reference pattern");
}
return super.visit(node, data);
}
}public class DetailedViolationRule extends AbstractVmRule {
@Override
public Object visit(ASTReference node, Object data) {
String refName = node.getRootString();
if (shouldReport(refName)) {
// Provide detailed context in violation message
String message = String.format(
"Reference '%s' at %s:%d violates naming convention. " +
"Expected pattern: camelCase starting with lowercase letter.",
refName, node.getTemplateName(), node.getLine()
);
addViolation(data, node, message);
}
return super.visit(node, data);
}
private boolean shouldReport(String refName) {
return refName != null &&
!refName.matches("^[a-z][a-zA-Z0-9]*$");
}
}Install with Tessl CLI
npx tessl i tessl/maven-net-sourceforge-pmd--pmd-vm