PMD JSP language module providing static code analysis capabilities for JavaServer Pages files with lexical analysis, AST parsing, and rule-based code quality checks.
—
PMD JSP provides a framework for creating custom JSP analysis rules. Rules implement the visitor pattern to analyze JSP AST and report code quality violations.
Base class for all JSP rules, implementing both PMD's rule interface and JSP visitor pattern.
public abstract class AbstractJspRule extends AbstractRule implements JspVisitor<Object, Object> {
public void apply(Node target, RuleContext ctx);
public Object visitNode(Node node, Object param);
}Usage:
import net.sourceforge.pmd.lang.jsp.rule.AbstractJspRule;
import net.sourceforge.pmd.lang.jsp.ast.*;
import net.sourceforge.pmd.reporting.RuleContext;
public class MyJspRule extends AbstractJspRule {
@Override
public Object visit(ASTElExpression node, Object data) {
// Rule implementation
if (violatesRule(node)) {
((RuleContext) data).addViolation(node, "Violation message");
}
return super.visit(node, data);
}
private boolean violatesRule(ASTElExpression node) {
// Custom rule logic
return node.getContent().contains("dangerousFunction");
}
}Methods:
apply(Node, RuleContext): Entry point called by PMD frameworkvisitNode(Node, Object): Default visitor implementation that traverses all childrenNote: The data parameter in visit methods is the RuleContext passed from the PMD framework and should be cast to RuleContext when reporting violations.
import net.sourceforge.pmd.lang.jsp.rule.AbstractJspRule;
import net.sourceforge.pmd.lang.jsp.ast.ASTJspScriptlet;
import net.sourceforge.pmd.reporting.RuleContext;
public class NoScriptletsRule extends AbstractJspRule {
@Override
public Object visit(ASTJspScriptlet node, Object data) {
// Report violation for any scriptlet
((RuleContext) data).addViolation(node,
"Avoid using JSP scriptlets. Use JSTL or EL expressions instead.");
return super.visit(node, data);
}
}import net.sourceforge.pmd.lang.jsp.rule.AbstractJspRule;
import net.sourceforge.pmd.lang.jsp.ast.*;
import net.sourceforge.pmd.reporting.RuleContext;
public class NoInlineStyleRule extends AbstractJspRule {
@Override
public Object visit(ASTElement node, Object data) {
// Check for style attributes
node.children(ASTAttribute.class)
.filter(attr -> "style".equals(attr.getName()))
.forEach(attr -> {
((RuleContext) data).addViolation(attr,
"Avoid inline styles. Use CSS classes instead.");
});
return super.visit(node, data);
}
}import net.sourceforge.pmd.lang.jsp.rule.AbstractJspRule;
import net.sourceforge.pmd.lang.jsp.ast.ASTElExpression;
import net.sourceforge.pmd.reporting.RuleContext;
public class UnsanitizedExpressionRule extends AbstractJspRule {
@Override
public Object visit(ASTElExpression node, Object data) {
String content = node.getContent();
// Check if expression is in a taglib context
boolean inTaglib = node.ancestors(ASTElement.class)
.anyMatch(elem -> elem.getNamespacePrefix() != null);
// Check if expression is sanitized
boolean sanitized = content.matches("^fn:escapeXml\\(.+\\)$");
if (!inTaglib && !sanitized) {
((RuleContext) data).addViolation(node,
"EL expression may be vulnerable to XSS attacks. " +
"Use fn:escapeXml() or place within a taglib element.");
}
return super.visit(node, data);
}
}import java.util.*;
public class DuplicateImportsRule extends AbstractJspRule {
private Set<String> seenImports = new HashSet<>();
@Override
public Object visit(ASTJspDirective node, Object data) {
if ("page".equals(node.getName())) {
// Find import attributes
node.children(ASTJspDirectiveAttribute.class)
.filter(attr -> "import".equals(attr.getName()))
.forEach(attr -> checkDuplicateImport(attr, data));
}
return super.visit(node, data);
}
private void checkDuplicateImport(ASTJspDirectiveAttribute attr, Object data) {
String importValue = attr.getValue();
if (!seenImports.add(importValue)) {
((RuleContext) data).addViolation(attr,
"Duplicate import: " + importValue);
}
}
@Override
public void start(RuleContext ctx) {
super.start(ctx);
seenImports.clear(); // Reset for each file
}
}public class NestedJsfInLoopRule extends AbstractJspRule {
@Override
public Object visit(ASTElement node, Object data) {
// Check for JSTL iteration tags
if (isJstlIterationTag(node)) {
// Look for JSF components within iteration
node.descendants(ASTElement.class)
.filter(this::isJsfComponent)
.forEach(jsfElement -> {
((RuleContext) data).addViolation(jsfElement,
"JSF component found within JSTL iteration. " +
"This can cause performance issues.");
});
}
return super.visit(node, data);
}
private boolean isJstlIterationTag(ASTElement element) {
return "c".equals(element.getNamespacePrefix()) &&
("forEach".equals(element.getLocalName()) ||
"forTokens".equals(element.getLocalName()));
}
private boolean isJsfComponent(ASTElement element) {
String prefix = element.getNamespacePrefix();
return "h".equals(prefix) || "f".equals(prefix);
}
}Based on the existing NoUnsanitizedJSPExpressionRule:
public class NoUnsanitizedJSPExpressionRule extends AbstractJspRule {
public Object visit(ASTElExpression node, Object data);
}Implementation pattern:
@Override
public Object visit(ASTElExpression node, Object data) {
if (elOutsideTaglib(node)) {
((RuleContext) data).addViolation(node);
}
return super.visit(node, data);
}
private boolean elOutsideTaglib(ASTElExpression node) {
ASTElement parentElement = node.ancestors(ASTElement.class).first();
boolean elInTaglib = parentElement != null &&
parentElement.getName() != null &&
parentElement.getName().contains(":");
boolean elWithFnEscapeXml = node.getContent() != null &&
node.getContent().matches("^fn:escapeXml\\(.+\\)$");
return !elInTaglib && !elWithFnEscapeXml;
}public class NoInlineStyleInformationRule extends AbstractJspRule {
// Implementation for detecting inline style attributes
}
public class NoLongScriptsRule extends AbstractJspRule {
// Implementation for detecting overly long scriptlets
}
public class NoScriptletsRule extends AbstractJspRule {
// Implementation for detecting JSP scriptlets
}public class DuplicateJspImportsRule extends AbstractJspRule {
// Implementation for detecting duplicate import directives
}Rules are defined in XML files under /category/jsp/:
<rule name="NoUnsanitizedJSPExpression"
language="jsp"
since="3.6"
message="Avoid unsanitized JSP expressions"
class="net.sourceforge.pmd.lang.jsp.rule.security.NoUnsanitizedJSPExpressionRule"
externalInfoUrl="${pmd.website.baseurl}/pmd_rules_jsp_security.html#nounsanitizedjspexpression">
<description>
Avoid unsanitized JSP expressions as they can lead to Cross Site Scripting (XSS) attacks.
</description>
<priority>2</priority>
<example>
<![CDATA[
<!-- Bad: Unsanitized EL expression -->
<p>${userInput}</p>
<!-- Good: Sanitized with fn:escapeXml -->
<p>${fn:escapeXml(userInput)}</p>
<!-- Good: Within taglib element -->
<c:out value="${userInput}"/>
]]>
</example>
</rule>Some rules use XPath expressions instead of Java implementations:
<rule name="NoClassAttribute"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<properties>
<property name="xpath">
<value><![CDATA[
//Attribute[ upper-case(@Name)="CLASS" ]
]]></value>
</property>
</properties>
</rule>public class ConfigurableRule extends AbstractJspRule {
private static final StringProperty MAX_LENGTH =
StringProperty.named("maxLength")
.desc("Maximum allowed length")
.defaultValue("100")
.build();
public ConfigurableRule() {
definePropertyDescriptor(MAX_LENGTH);
}
@Override
public Object visit(ASTJspScriptlet node, Object data) {
int maxLength = Integer.parseInt(getProperty(MAX_LENGTH));
if (node.getContent().length() > maxLength) {
((RuleContext) data).addViolation(node,
"Scriptlet exceeds maximum length of " + maxLength);
}
return super.visit(node, data);
}
}public class StatefulRule extends AbstractJspRule {
private Map<String, Integer> elementCounts = new HashMap<>();
@Override
public void start(RuleContext ctx) {
super.start(ctx);
elementCounts.clear();
}
@Override
public Object visit(ASTElement node, Object data) {
String elementName = node.getName();
int count = elementCounts.getOrDefault(elementName, 0) + 1;
elementCounts.put(elementName, count);
if (count > 10) {
((RuleContext) data).addViolation(node,
"Too many " + elementName + " elements (" + count + ")");
}
return super.visit(node, data);
}
}public class ComplexAnalysisRule extends AbstractJspRule {
private Stack<String> contextStack = new Stack<>();
private boolean inFormContext = false;
@Override
public Object visit(ASTElement node, Object data) {
String elementName = node.getName();
// Track context
if ("form".equals(elementName)) {
inFormContext = true;
contextStack.push("form");
}
// Analyze based on context
if ("input".equals(elementName) && !inFormContext) {
((RuleContext) data).addViolation(node,
"Input element found outside of form context");
}
// Continue traversal
Object result = super.visit(node, data);
// Clean up context
if ("form".equals(elementName)) {
contextStack.pop();
inFormContext = !contextStack.contains("form");
}
return result;
}
}import net.sourceforge.pmd.test.SimpleAggregatorTst;
public class MyJspRuleTest extends SimpleAggregatorTst {
private static final String TEST_JSP =
"<%@ page language=\"java\" %>\n" +
"<html>\n" +
" <body>\n" +
" ${unsafeExpression}\n" +
" </body>\n" +
"</html>";
@Test
public void testUnsafeExpression() {
Rule rule = new NoUnsanitizedJSPExpressionRule();
RuleViolation[] violations = getViolations(rule, TEST_JSP);
assertEquals(1, violations.length);
assertTrue(violations[0].getDescription().contains("unsafe"));
}
}Rules are organized into categories:
Each category has its own XML ruleset file that can be included or excluded in PMD analysis configurations.
Rules integrate with PMD's analysis engine:
The rule framework provides a powerful foundation for implementing custom JSP code quality checks that integrate seamlessly with PMD's analysis pipeline.
Install with Tessl CLI
npx tessl i tessl/maven-net-sourceforge-pmd--pmd-jsp