PMD Custom Rulesets¶
The custom rulesets of Appendix F.2, in full, one tab for each file. A project repository keeps each file in config/pmd/ (The PMD Folder), and code-analyzer.yml lists each of them under engines.pmd.custom_rulesets.
The custom rules for Apex.
Apex
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="DevSecOps Reference Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
Custom PMD rules for Apex, written in XPath. The other custom rules are in
pmd-custom-rules-markup.xml, pmd-custom-rules-metadata.xml,
pmd-custom-rules-flow.xml, and, for JavaScript,
pmd-custom-rules-javascript.xml. The stock PMD rules this reference adopts
are in pmd-stock-rules.xml and, for JavaScript, in
pmd-stock-rules-javascript.xml. Adjust priorities and suppressions to
match your organisation's risk posture and coding standards.
</description>
<!--
Rule messages contain no apostrophes. PMD formats a message with Java's
MessageFormat, which treats a single apostrophe as a quoting character
and drops it, so "class's" would be printed as "classs".
-->
<exclude-pattern>.*/.sfdx/.*</exclude-pattern>
<exclude-pattern>.*/.sf/.*</exclude-pattern>
<exclude-pattern>.*/node_modules/.*</exclude-pattern>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX ACCESS, SHARING, AND ACCESS MODE -->
<!-- ============================================================ -->
<!--
Requires every Apex method, test methods included, to declare an explicit
access modifier. The body of a trigger is exempt: PMD models it as a method,
and it cannot carry a modifier.
Methods without an explicit access modifier default to private in Apex.
While the default behavior is conservative, explicit modifiers improve
readability and make intent unambiguous.
-->
<rule
name="DeclareAccessOnMethods"
message="Explicitly use public/private/protected/global on Apex methods"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>Every method that is not a test method must declare an explicit access modifier: instance and static methods, constructors, and the helper and setup methods of a test class. Apex treats a method without a modifier as private, so this rule changes no behaviour; it exists for readability and auditability, so that a reader or reviewer never has to know the default.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Method[@StaticInitializer = false()][not(parent::Property)][not(parent::UserTrigger and @RealLoc = false())][ModifierNode[@Public = false() and @Private = false() and @Protected = false() and @Global = false() and @Test = false()]]
]]></value>
</property>
</properties>
</rule>
<!--
Flags use of without sharing on any class.
Classes using without sharing should include a comment explaining
the business justification. The rule applies to all classes including
test classes - the requirement for documented justification is not
relaxed in a test context.
-->
<rule
name="DoNotUseWithoutSharing"
message="Use with sharing or inherited sharing, or document the justification for without sharing"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>Flags any class declared as without sharing.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass/ModifierNode[@WithoutSharing=true()]
]]></value>
</property>
</properties>
</rule>
<rule
name="RequireExplicitSharing"
message="Declare the sharing of this class: with sharing, without sharing, or inherited sharing"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>Every outer class must declare its sharing. The reportInTestClasses property decides whether test classes are reported. Interfaces and enums are never checked, and an inner class without a sharing declaration takes the sharing of the class that calls it, so no declaration is needed there.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[@Nested = false()][
ModifierNode[
@WithoutSharing = false() and
@WithSharing = false() and
@InheritedSharing = false() and
($reportInTestClasses or @Test = false())
]
]
]]></value>
</property>
<property
name="reportInTestClasses"
type="Boolean"
value="true"
description="Whether a test class without a sharing declaration is reported" />
</properties>
</rule>
<rule
name="RequireInnerClassSharing"
message="Declare the sharing of this inner class: with sharing, without sharing, or inherited sharing"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>An inner class should declare its sharing. An inner class with no declaration takes the sharing of the class that calls it, so the declaration is a recommendation and not a need: with it written, a reader knows how the class runs without tracing its callers.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[@Nested = true()]
[not(ends-with(lower-case(@SuperClassName), 'exception'))]
[not(ancestor::UserClass[ModifierNode[@Test = true()]])]
[not(ancestor::UserClass[@SuperClassName != '' or string-join(@InterfaceNames, '') != ''])]
[
ModifierNode[
@WithoutSharing = false() and
@WithSharing = false() and
@InheritedSharing = false()
]
]
]]></value>
</property>
</properties>
</rule>
<rule
name="RequireOuterClassAccess"
message="Specify public, private, or global on outer Apex classes"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>All outer Apex classes must have an explicit access modifier.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/ApexFile/UserClass[
ModifierNode[
@Public = false() and
@Private = false() and
@Global = false()
] and
@Nested = false()
]
]]></value>
</property>
</properties>
</rule>
<rule
name="DoNotRunAsCurrentUser"
message="Do not execute test methods as the running user - use a dedicated test user instead"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>Tests must not run as the current user, for example System.runAs(new User(Id = UserInfo.getUserId())). The rule is deliberately over-strict: any UserInfo.getUserId() call inside a System.runAs statement is reported because this is a screening rule. Methods annotated @TestSetup are exempt.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//RunAsBlockStatement[.//MethodCallExpression[lower-case(@FullMethodName) = 'userinfo.getuserid'] and ancestor::Method/ModifierNode[count(Annotation[lower-case(@Name) = 'testsetup']) = 0]]
]]></value>
</property>
</properties>
</rule>
<!--
Access mode. The standard is enforced by two custom rules written in
Java, RequireAccessMode and FlagSystemMode. XPath can do neither
job: both read the DML access mode, which PMD does not expose to
XPath (pmd/pmd#7108). The one access mode rule that XPath can
express in full is here: WITH SECURITY_ENFORCED is rejected.
-->
<rule
name="NoSecurityEnforced"
message="Replace WITH SECURITY_ENFORCED with WITH USER_MODE"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="apex">
<description>WITH SECURITY_ENFORCED is not accepted in a query. WITH USER_MODE replaces it, and is stricter.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//SoqlExpression[matches(@Query, 'WITH\s+SECURITY_ENFORCED', 'i')] | //LiteralExpression[@String = true()][matches(@Image, '(^\s*|(SELECT|FROM|WHERE)\s.*)WITH\s+SECURITY_ENFORCED', 'is')]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX ERROR PRONE -->
<!-- ============================================================ -->
<!--
Reports a switch statement with no when else block. A switch on a
value, an sObject type, or an enum handles the values it names and
falls through for every other, so a new picklist value or enum value
arrives unhandled with no sign of it. The when else block says what
happens then, even when that is nothing but a comment.
-->
<rule
name="SwitchWithoutWhenElse"
language="apex"
message="Add a when else block to this switch statement, to say what happens for a value it does not name"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every switch statement has a when else block, so that a value the switch does not name is handled on purpose. A switch with no when else is reported and does not block.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//SwitchStatement[not(ElseWhenBlock)]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an if statement, with its else-if branches, whose every
condition compares one variable with a value that a when clause can
hold, when it makes minConditions or more such comparisons (3 by
default). A condition may join comparisons with ||, and each one
counts: if (s == 'a' || s == 'b') {} else if (s == 'c') {} makes
three. A value is a String, Integer, or Long literal (a negative number
included), null, or a name such as Status.ACTIVE whose first part is
not a variable of the method or the class, as an enum value is. A
comparison with null may be in the chain, but it is not counted. The
variable is on either side of ==, and it may be a field of a record
(acc.Type); this.name and name are the same variable. A chain of
instanceof tests is reported as well, when the variable is declared
as SObject in the method or the class and no condition uses ||. An
else branch does not stop the report: it becomes when else.
Not reported: a condition with any other operator (!=, &&, <, a
method call), a variable declared in the method or the class with a
type that switch does not take (Id, Decimal, Double, Boolean, Date,
Datetime, Time, Blob, Object, or SObjectType), and separate if
statements that follow one another, because each of them runs and a
switch runs only one branch. Names are compared without regard to case.
The type of a variable declared in another file is not known, so a
chain on such a variable is read from its values alone, and a static
final constant reached through its class (Constants.ACTIVE, or the
class's own name) looks like an enum value.
Adapted from PreferSwitchOverIfElseChains of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="PreferSwitchOverIfElseChains"
language="apex"
message="This if statement compares one variable with ${minConditions} or more values. Use a switch statement on the variable"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An if statement whose branches compare one variable with a list of values reads as a list of cases, and a switch statement says that directly: each value is written once, in a when clause, and the variable is written once. The minConditions property sets how many comparisons make the if statement a switch, and it is 3 by default, so a test of one or two values stays an if statement. Comparisons with null are not counted. Check the change: == compares two strings without regard to case, but a when clause on a String is case-sensitive. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//IfElseBlockStatement[
let $conditions := IfBlockStatement/StandardCondition/*,
$ors := $conditions/descendant-or-self::BooleanExpression[@Op = '||'],
$leaves := ($conditions | $ors/*)[not(self::BooleanExpression[@Op = '||'])],
$method := ancestor::Method[1],
$class := ancestor::UserClass[1],
$fields := ($class/Field | $class/Property/Field),
$declared := ($method//VariableDeclaration/lower-case(@Image), $method/Parameter/lower-case(@Image), $fields/lower-case(@Name)),
$isValue := function($node) {
exists($node/self::LiteralExpression[@LiteralType = ('STRING', 'INTEGER', 'LONG', 'NULL')]
| $node/self::PrefixExpression[@Op = '-'][LiteralExpression[@LiteralType = ('INTEGER', 'LONG')]]
| $node/self::VariableExpression[ReferenceExpression[@Image != ''][not(*)][not(lower-case(@Image) = $declared)]])
},
$name := function($node) {
lower-case(string-join(($node/ReferenceExpression/@Image[. != ''], $node/@Image), '.'))
},
$subjects := function($leaf) {
if ($leaf/self::InstanceOfExpression) then $leaf/VariableExpression ! $name(.)
else for $operand in $leaf/VariableExpression return (if ($isValue($leaf/*[not(. is $operand)])) then $name($operand) else ())
},
$subject := (for $candidate in $subjects($leaves[1]) return $candidate[every $leaf in $leaves satisfies $candidate = $subjects($leaf)])[1],
$type := lower-case((
$method//VariableDeclaration[lower-case(@Image) = $subject]/@Type,
$method/Parameter[lower-case(@Image) = $subject]/@Type,
$fields[lower-case(@Name) = $subject]/@Type
)[1])
return exists($leaves)
and (every $leaf in $leaves satisfies $leaf[self::BooleanExpression[@Op = '=='] or self::InstanceOfExpression])
and exists($subject)
and not($type = ('id', 'decimal', 'double', 'boolean', 'date', 'datetime', 'time', 'blob', 'object', 'sobjecttype', 'schema.sobjecttype'))
and (not($leaves/self::InstanceOfExpression) or ($type = 'sobject' and empty($ors)))
and count($leaves[not(*/@LiteralType = 'NULL')]) >= $minConditions
]
]]></value>
</property>
<property
name="minConditions"
type="Integer"
value="3"
description="The number of comparisons of one variable that make an if statement a switch statement" />
</properties>
</rule>
<!--
Flags a null check that comes after the collection has already been
used: `accts.isEmpty() && accts != null` throws before the guard runs.
The guard must come first. The rule looks at each `&&` whose left
operand calls isEmpty() and whose right operand is a `!= null` test;
it does not report isEmpty() with no guard at all, which is usually a
collection the method itself created.
-->
<rule
name="NullValueCheckBeforeEmptyCheck"
language="apex"
message="Check for null before calling isEmpty() on a collection, not after"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A null check placed after an isEmpty() call on the same expression runs too late; the isEmpty() call throws first. Put the null check first.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//BooleanExpression[@Op = '&&'][*[1][descendant-or-self::MethodCallExpression[lower-case(@MethodName) = 'isempty']]][*[2][self::BooleanExpression[@Op = '!='][*[1][self::VariableExpression] and *[2][self::LiteralExpression[@LiteralType = 'NULL']]]]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports every call to getRecordTypeInfosByName(), which looks a record
type up by its label. A label can be changed in Setup, so the lookup
breaks without a deployment. getRecordTypeInfosByDeveloperName() and
getRecordTypeInfosById() are not reported.
-->
<rule
name="AvoidRecordTypeInfosByName"
language="apex"
message="Look the record type up by its developer name - use getRecordTypeInfosByDeveloperName()"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>getRecordTypeInfosByName() finds a record type by its label, which can be changed in Setup at any time. Find it by its developer name, which does not change, with getRecordTypeInfosByDeveloperName().</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@MethodName) = 'getrecordtypeinfosbyname']
]]></value>
</property>
</properties>
</rule>
<!--
Reports an assignment whose two sides are the same expression,
compared by their syntax tree, such as count = count, this.name =
this.name, or acc.Name = acc.Name. this.name = name is not reported.
The name ends in Apex because PMD ships a Java rule named
IdempotentOperations. From the CodeScan rule sf:IdempotentOperations.
-->
<rule
name="IdempotentOperationsApex"
language="apex"
message="This assignment gives a variable its own value, so it does nothing; assign the value that was meant"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An assignment changes something. An assignment of a variable or a field to itself, such as count = count, does nothing, and is usually a mistake for this.name = name or for another value.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//AssignmentExpression[@Op = '='][
string-join(*[1]/descendant-or-self::*/concat(name(), ':', @Image, ':', @Op, ':', @MethodName, ':', @LiteralType), '|') = string-join(*[2]/descendant-or-self::*/concat(name(), ':', @Image, ':', @Op, ':', @MethodName, ':', @LiteralType), '|')
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an else if whose condition is the same expression, compared by
its syntax tree, as an earlier condition of the same if statement. A
separate if statement with the same condition is not reported, and
neither is a condition that means the same thing in other words, such
as s == 'a' and s.equals('a'). From the CodeScan rule
sf:FixDuplicateConditions.
-->
<rule
name="FixDuplicateConditions"
language="apex"
message="This condition is the same as an earlier condition in this if statement, so this branch can never run"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Each branch of an if / else if chain tests a different condition. A condition that repeats an earlier one in the same chain can never be true when it is reached, so its branch never runs.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//IfElseBlockStatement/IfBlockStatement[
let $s := string-join(StandardCondition/descendant-or-self::*/concat(name(), ':', @Image, ':', @Op, ':', @MethodName, ':', @LiteralType), '|')
return preceding-sibling::IfBlockStatement[string-join(StandardCondition/descendant-or-self::*/concat(name(), ':', @Image, ':', @Op, ':', @MethodName, ':', @LiteralType), '|') = $s]
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports x != null || followed by a use of x, and x == null and-and
followed by a use of x, where the use does not have the safe
navigation operator. Only the usual forms, with the null check first
and a plain variable, are found. The name ends in Apex because PMD
ships a Java rule named BrokenNullCheck. From the CodeScan rule
sf:BrokenNullCheck.
-->
<rule
name="BrokenNullCheckApex"
language="apex"
message="This null check throws the null error it is meant to prevent; use and in place of or, or or in place of and"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A null check protects the expression after it. x != null || x.size() > 0 and x == null && x.isEmpty() both read the variable exactly when it is null, so they throw the error that the check was written to prevent.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//BooleanExpression[@Op = '||'][*[1][self::BooleanExpression[@Op = '!='][LiteralExpression[@LiteralType = 'NULL']]]]
[let $v := *[1]/VariableExpression/@Image return *[2]//ReferenceExpression[@Image = $v][@SafeNav = false()]]
|
//BooleanExpression[@Op = '&&'][*[1][self::BooleanExpression[@Op = '=='][LiteralExpression[@LiteralType = 'NULL']]]]
[let $v := *[1]/VariableExpression/@Image return *[2]//ReferenceExpression[@Image = $v][@SafeNav = false()]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an if or else if whose condition is true, false, its negation,
or a comparison of two literals, such as 1 == 1. A variable or a
constant is not followed, because PMD does not know its value. The
name ends in Apex because PMD ships a Java rule named
UnconditionalIfStatement. From the CodeScan rule
sf:UnconditionalIfStatement.
-->
<rule
name="UnconditionalIfStatementApex"
language="apex"
message="This condition is always true or always false; remove the if statement or the code that never runs"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An if statement tests something that can change. A condition written as true, false, or a comparison of two literals is always the same, so the if statement runs its block always or never. A constant that holds such a value is not seen.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//IfBlockStatement[StandardCondition[
LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]
or PrefixExpression[@Op = '!']/LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]
or BooleanExpression[count(*) = 2][count(LiteralExpression) = 2]
]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports return null in a method whose return type is a List, a Set, a
Map, or an array. From the CodeScan rule
sf:ReturnEmptyArrayRatherThanNull.
-->
<rule
name="ReturnEmptyArrayRatherThanNull"
language="apex"
message="Return an empty collection in place of null, so that the caller can loop over the result"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A method that returns a List, a Set, a Map, or an array returns an empty one, not null, so that every caller can loop over the result without a null check.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Method[matches(@ReturnType, '^(List|Set|Map)\s*<|\[\]$', 'i')]//ReturnStatement[LiteralExpression[@LiteralType = 'NULL']]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a != null and-and b.equals(a), where the variable checked for
null is the argument of equals() and not its receiver. A literal
receiver, as in 'x'.equals(a), is not reported. The name ends in Apex
because PMD ships a Java rule named UnusedNullCheckInEquals. From the
CodeScan rule sf:UnusedNullCheckInEquals.
-->
<rule
name="UnusedNullCheckInEqualsApex"
language="apex"
message="This null check protects the wrong object; call equals on the object that was checked"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A null check before an equals() call protects the object whose method is called. a != null && b.equals(a) checks a and calls a method of b, which can still be null.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//BooleanExpression[@Op = '&&'][
let $v := BooleanExpression[@Op = '!='][LiteralExpression[@LiteralType = 'NULL']]/VariableExpression/@Image
return MethodCallExpression[lower-case(@MethodName) = 'equals'][not(ReferenceExpression/LiteralExpression)]
[VariableExpression/@Image = $v][not(ReferenceExpression/@Image = $v)]
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports equals(), equalsIgnoreCase(), or compareTo() called on a
variable or an expression with a string literal as its argument. The
== operator, which does not fail on null, is not reported. From the
CodeScan rule sf:PositionLiteralsFirstInComparisons.
-->
<rule
name="PositionLiteralsFirstInComparisons"
language="apex"
message="Write the string literal first, as in the literal.equals(value), so that a null value cannot cause an error"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A comparison with a string literal calls the method on the literal: 'abc'.equals(s), not s.equals('abc'), which fails when s is null.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@MethodName) = ('equals', 'equalsignorecase', 'compareto')][@InputParametersSize = 1]
[LiteralExpression[@String = true()]][not(ReferenceExpression/LiteralExpression)]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an abstract class, top-level or nested, that extends no class
and declares no method and no constructor. A class that extends
another inherits its methods and is not reported. The name ends in
Apex because PMD ships a Java rule named
AbstractClassWithoutAnyMethod. From the CodeScan rule
sf:AbstractClassWithoutAnyMethod.
-->
<rule
name="AbstractClassWithoutAnyMethodApex"
language="apex"
message="Add a method to this abstract class, or remove the abstract modifier"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An abstract class exists to be completed by a subclass, so it declares at least one method or constructor. An abstract class with only fields is a data holder that no code can create, and is not abstract.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[ModifierNode/@Abstract = true()][@SuperClassName = ''][not(Method)]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a top-level class whose constructors are all private and that
has no public or global static method, field, or property. A constants
class with public static fields, and a class with a static factory
method, are not reported. The name ends in Apex because PMD ships a
Java rule named MissingStaticMethodInNonInstantiatableClass. From the
CodeScan rule sf:MissingStaticMethodInNonInstantiatableClass.
-->
<rule
name="MissingStaticMethodInNonInstantiatableClassApex"
language="apex"
message="Nothing can use this class - add a public static member, or make a constructor public"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A class that code cannot create, because every constructor is private, has a public or global static member, such as a factory method or a constant, or no code can use it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/ApexFile/UserClass
[Method[@Constructor = true()]]
[not(Method[@Constructor = true()]/ModifierNode[@Public = true() or @Protected = true() or @Global = true()])]
[not(Method[not(@StaticInitializer = true())]/ModifierNode[@Static = true()][@Public = true() or @Global = true()])]
[not(Field/ModifierNode[@Static = true()][@Public = true() or @Global = true()])]
[not(Property/ModifierNode[@Static = true()][@Public = true() or @Global = true()])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an empty instance initializer block, a pair of braces directly
in a class. The stock rule EmptyStatementBlock reports an empty static
initializer and not this form. From the CodeScan rule
sf:EmptyInitializer.
-->
<rule
name="EmptyInitializer"
language="apex"
message="Remove this empty initializer block, or put in it the code it was written for"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An instance initializer block holds the code it was written for. An empty one does nothing, and a reader cannot tell whether code was lost from it.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass/BlockStatement[count(*) = 0]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a switch statement inside another switch statement in the same
method. A switch in a method that a when block calls is not reported.
From the CodeScan rule sf:AvoidNestedSwitchStmts.
-->
<rule
name="AvoidNestedSwitchStmts"
language="apex"
message="Move the inner switch statement to a method of its own"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A switch statement does not hold another switch statement. The inner one moves to a method of its own, whose name says what it decides.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//SwitchStatement[ancestor::SwitchStatement]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX DOCUMENTATION -->
<!-- ============================================================ -->
<!--
Reports an ApexDoc comment whose description is shorter than 10
characters. Salesforce writes the description as the text at the top of
the comment, before any tag; a comment may also write it after a
@description tag. PMD exposes an ApexDoc block to XPath as a
FormalComment node whose Image is the whole comment, unlike an
ordinary comment, which XPath cannot see. The rule takes the text after
the @description tag when there is one, and otherwise the text before the
first tag, cuts it at the next ApexDoc block tag or the end of the
comment, turns each line's leading asterisk into a space, collapses the
whitespace, and counts from the first letter, spaces included. A comment
with no description at all is the stock ApexDoc rule's to report.
-->
<rule
name="ApexDocDescriptionLength"
language="apex"
message="Write a description of at least 10 characters at the top of the ApexDoc comment"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The description of an ApexDoc comment, the text at its top or the text after a @description tag, is at least 10 characters long, counted from its first letter and including spaces, across the lines it wraps onto. A shorter one, such as a placeholder, says nothing, and is reported without blocking.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//FormalComment[
let $c := @Image,
$d := if (contains($c, '@description')) then substring-after($c, '@description') else replace($c, '^\s*/\*+', ''),
$t := normalize-space(replace(replace(replace($d, '(^|[\s*])@(param|return|throws|exception|deprecated|since|see|author|version|group|example)(\s|$).*$', '', 'is'), '(^|\n)\s*\*+/?', ' ', 's'), '\*+/\s*$', ''))
return string-length($t) < 10 and (contains($c, '@description') or string-length($t) > 0)
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a method that has an ApexDoc comment and a throw statement
whose exception the comment does not name in a @throws tag. Each type
thrown needs its own tag. The type of a throw is read from new X(...),
from the catch clause that declares a rethrown variable, or from a
local variable or parameter of that name; names are compared without
regard to case, and by the part after the last dot, so
@throws MyClass.MyException and @throws MyException both name
new MyClass.MyException(). When the type cannot be read, as in
throw buildException(), any @throws tag is enough. A throw inside a
try block whose own catch clause takes that type, or Exception, never
leaves the method and is not counted; the rule does not know which
exception types extend which others. The @exception tag is not read
as a synonym of @throws. A method without an ApexDoc comment is not
reported.
Adapted from ExceptionDocumentationRequired of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="ExceptionDocumentationRequired"
language="apex"
message="Name each exception this method throws in a @throws tag of its ApexDoc comment"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The ApexDoc comment of a method that throws an exception should name each exception type it throws in a @throws tag, so that a caller knows what to catch. An exception that the method catches itself is not counted, and a method without an ApexDoc comment is not reported. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Method[FormalComment][
let $tags := for $part in tokenize(lower-case(FormalComment/@Image), '@throws\s+')[position() > 1]
return tokenize(tokenize($part, '[^\w.]')[1], '\.')[last()]
return some $throw in .//ThrowStatement satisfies (
let $name := $throw/VariableExpression[not(*[not(self::EmptyReferenceExpression)])]/lower-case(@Image),
$declared := (
$throw/NewObjectExpression/@Type,
$throw/ancestor::CatchBlockStatement[lower-case(@VariableName) = $name][1]/@ExceptionType,
.//VariableDeclaration[lower-case(@Image) = $name]/@Type,
Parameter[lower-case(@Image) = $name]/@Type
)[1],
$type := replace(lower-case(string($declared)), '^.*\.', '')
return not($throw/ancestor::BlockStatement[parent::TryCatchFinallyBlockStatement][not(preceding-sibling::*)]
/following-sibling::CatchBlockStatement[replace(lower-case(@ExceptionType), '^.*\.', '') = ('exception', $type)])
and (if ($type = '') then empty($tags) else not($type = $tags))
)
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an element that has the @Deprecated annotation and no @deprecated
tag in its ApexDoc comment, or a tag with no text after it. Salesforce's
Apex Developer Guide says to include the tag, with the reason for the
deprecation and the recommended alternative. The rule reads the ApexDoc
comment as the whole text of a FormalComment node, as
ApexDocDescriptionLength does, and finds the tag as a word that follows
whitespace or an asterisk. It cuts the text after the tag at the next
ApexDoc block tag or the end of the comment. It cannot tell whether that
text gives a reason and an alternative; a reviewer checks that. A
deprecated element with no comment at all is reported too. PMD does not
keep the comment of an enum in the tree, so an enum is never reported. A
property is read once, and the accessor methods and the field that PMD
makes for it are skipped.
-->
<rule
name="DeprecatedDocumentationRequired"
language="apex"
message="Give this deprecated element a @deprecated tag in its ApexDoc comment, with the reason and the alternative"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An element with the @Deprecated annotation has a @deprecated tag in its ApexDoc comment, and the tag has text after it. The text gives the reason for the deprecation and the recommended alternative, so that a developer who uses the element knows what to use instead. A deprecated element with no comment, with no tag, or with an empty tag is reported without blocking. An enum is not read, because PMD does not keep the comment of an enum.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[ModifierNode/Annotation[lower-case(@Name) = 'deprecated']][not(self::Field or self::UserEnum or parent::Property)][
let $c := string((FormalComment | FieldDeclaration/FormalComment)[1]/@Image),
$has := matches($c, '(^|[\s*])@deprecated(\s|$)', 'i'),
$after := replace($c, '^.*?(^|[\s*])@deprecated(\s|$)', '', 'is'),
$t := normalize-space(replace(replace(replace($after, '(^|[\s*])@(param|return|throws|exception|deprecated|since|see|author|version|group|example|description)(\s|$).*$', '', 'is'), '(^|\n)\s*\*+/?', ' ', 's'), '\*+/\s*$', ''))
return not($has and string-length($t) > 0)
]
]]></value>
</property>
</properties>
</rule>
<!--
Two rules on the form of an ApexDoc tag. Each reads the ApexDoc comment
as the whole text of a FormalComment node, as ApexDocDescriptionLength
does, and finds a tag as an @ that follows whitespace or an asterisk,
so an email address or a URL in the text is not read as a tag. The
colon rule also reads an @ after the curly bracket of an inline tag,
such as {@code}. A comment is reported once, on its first
line, however many of its tags break the rule. PMD does not keep the
comment of a top-level enum, or of an inner enum that no other member
follows, so such a comment is never reported.
-->
<rule
name="NoColonAfterApexDocTag"
language="apex"
message="Remove the colon after each ApexDoc tag in this comment, and write the text of the tag after its name and a space"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The text of an ApexDoc tag follows the name of the tag, with no colon between them. A colon after a tag, with or without whitespace before it, is read as the first character of the text, so generated documentation shows it. The rule reads every tag, block and inline, whatever its name.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//FormalComment[matches(@Image, '(^|[\s*{])@[A-Za-z]+\s*:')]
]]></value>
</property>
</properties>
</rule>
<rule
name="NoLastModifiedApexDocTag"
language="apex"
message="Remove the @last modified tags from this ApexDoc comment, because git records who changed the file and when"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An ApexDoc comment has no @last modified tag, such as @last modified on or @last modified by, in any case and with or without a colon. No such tag is an ApexDoc tag: a header template wrote it, and version control records who changed a file and when. The tag becomes wrong after the next change that does not update it.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//FormalComment[matches(@Image, '(^|[\s*])@last[\s_-]*modified', 'i')]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX NAMING -->
<!-- ============================================================ -->
<!--
Two rules on a field or property that shares a name with something else
in its class. Apex compiles both, because fields, methods, and types
are looked up separately, and names are compared without regard to
case. A method is matched by name alone, whatever its parameters; a
constructor is not a method here; and an inner class is compared with
its own methods and its own name.
-->
<rule
name="FieldNameMatchesMethodName"
language="apex"
message="Rename this field or the method that has the same name"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A field or property with the same name as a method of its class makes each harder to tell from the other where it is used. Apex names are case-insensitive, so two names that differ only in case are the same name.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Field[lower-case(@Name) = ancestor::UserClass[1]/Method[@Constructor = false()]/lower-case(@Image)]
]]></value>
</property>
</properties>
</rule>
<rule
name="FieldNameMatchesClassName"
language="apex"
message="Rename this variable - it has the same name as a class"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A field or property with the same name as the class that declares it reads as a reference to the class, and so does a local variable with the name of any class declared in the same file. Apex names are case-insensitive, so two names that differ only in case are the same name.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Field[lower-case(@Name) = lower-case(ancestor::UserClass[1]/@SimpleName)]
| //VariableDeclaration[lower-case(@Image) = (/ApexFile//UserClass/lower-case(@SimpleName))]
]]></value>
</property>
</properties>
</rule>
<!--
A top-level class with an @AuraEnabled method is the server side of a
Lightning component, Aura or Lightning Web Component, and is named as a
controller. A class that only has @AuraEnabled fields is a data class
and is not reported, and neither is an inner class. The
controllerPattern property is a regular expression that the whole class
name must match.
-->
<rule
name="AuraEnabledControllerNaming"
language="apex"
message="Rename this class - a class with @AuraEnabled methods has a name that matches ${controllerPattern}"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A top-level class with an @AuraEnabled method is called from a Lightning component, and its name says so. The controllerPattern property holds the pattern the name must match, and each organisation sets its own.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[@Nested = false()][not(matches(@SimpleName, concat('^(', $controllerPattern, ')$')))][Method/ModifierNode/Annotation[lower-case(@Name) = 'auraenabled']]
]]></value>
</property>
<property
name="controllerPattern"
type="String"
value=".*Controller"
description="A regular expression that the name of a class with @AuraEnabled methods must match" />
</properties>
</rule>
<rule
name="TriggerNameSuffix"
language="apex"
message="End the trigger name with the suffix ${suffixes}"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A trigger's name ends with one of the suffixes in the suffixes property, compared without regard to case, so that every trigger in the org is named the same way. Each organisation sets the list to its own convention.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserTrigger[not(matches(@Image, concat('(', string-join($suffixes, '|'), ')$'), 'i'))]
]]></value>
</property>
<property
name="suffixes"
type="List[String]"
value="Trigger"
description="The suffixes a trigger name may end with; a name that ends with any of them passes" />
</properties>
</rule>
<!--
A variable with a one-letter name: a local variable (a loop variable
included), a parameter, a field, a property, or the variable of a
catch block. An enum value is not a variable and is not read. Only the
length of the name is read, so the case of the letter does not matter.
A variable in a test class, an inner class of a test class included, is
reported only when the reportInTestClasses property is true, and it is
false by default.
-->
<rule
name="NoSingleLetterVariableName"
language="apex"
message="Give this variable a name of two or more letters"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A variable with a one-letter name says nothing about what it holds, so a reader has to read the code around it to find out. A local variable, a loop variable, a parameter, a field, a property, and the variable of a catch block each have a name of two or more letters that says what the variable holds. Three names are allowed in their place: i and j as the counter of a for loop, and e as the variable of a catch block. The allowedNames property lists other one-letter names that are allowed for every kind of variable, and it is empty by default. An enum value is not a variable and is not reported. The reportInTestClasses property decides whether a test class is reported, and it is false by default.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
let $allowed := for $name in $allowedNames return lower-case($name)
return (
//VariableDeclaration[string-length(@Image) = 1]
[not(lower-case(@Image) = ('i', 'j') and parent::VariableDeclarationStatements/parent::ForLoopStatement)]
[not(lower-case(@Image) = $allowed)]
| //Field[@RealLoc = true()][not(parent::UserEnum)][string-length(@Name) = 1][not(lower-case(@Name) = $allowed)]
| //Parameter[@RealLoc = true()][string-length(@Image) = 1][not(lower-case(@Image) = $allowed)]
| //CatchBlockStatement[string-length(@VariableName) = 1][not(lower-case(@VariableName) = ('e', $allowed))]
)[$reportInTestClasses or not(ancestor::UserClass/ModifierNode[@Test = true()])]
]]></value>
</property>
<property
name="allowedNames"
type="List[String]"
value=""
description="Other one-letter names that are allowed for every kind of variable, compared without regard to case" />
<property
name="reportInTestClasses"
type="Boolean"
value="false"
description="Whether a variable in a test class is reported" />
</properties>
</rule>
<!--
Reports a method with no parameters whose name starts with get and
that returns a Boolean. A method with a parameter is not reported. The
name ends in Apex because PMD ships a Java rule named
BooleanGetMethodName. From the CodeScan rule sf:BooleanGetMethodName.
-->
<rule
name="BooleanGetMethodNameApex"
language="apex"
message="Name this method as a question, such as isActive or hasAccess, in place of get"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A method that returns a Boolean has a name that reads as a question, such as isActive or hasAccess, not getActive.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Method[matches(@Image, '^get', 'i')][lower-case(@ReturnType) = 'boolean'][@Arity = 0]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX READABILITY -->
<!-- ============================================================ -->
<!--
Reports a ternary that only supplies a default for a null value: the
condition compares an expression with null, using == or != with null
on either side, and the branch that runs when the expression is not
null is the expression itself, or a call or a field read on it. So
x != null ? x : d, x == null ? d : x, and null == x ? d : x are
reported, and are written x ?? d; obj != null ? obj.m() : d and
obj != null ? obj.Field : d are reported, and are written
obj?.m() ?? d and obj?.Field ?? d. The second form also returns d
when obj.m() itself returns null, where the ternary returned null.
The expression is matched to the branch by name, without regard to
case: a variable, a field read through this, a field read through one
variable (acc.Owner), or a call with no arguments. PMD keeps only the
first and last names of a longer dotted name, so acc.Owner.Name and
acc.Parent.Name look the same to the rule. A call or field read on the
value is matched only when the value is a plain variable. A ternary in
a SOQL bind expression is not reported, because neither operator can
be used there. A ternary that tests anything else, or whose branch is
some other value, is not reported.
Adapted from PreferNullCoalescingOverTernary of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="PreferNullCoalescingOverTernary"
language="apex"
message="Write this null check with the null coalescing operator (??), and with the safe navigation operator (?.) for a call or a field on the value"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A ternary that only supplies a default for a null value is written with the null coalescing operator (??), with the safe navigation operator (?.) where the value is a call or a field on an object. The shorter form names the value once, and it says plainly that the value has a default. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//TernaryExpression[not(ancestor::BindExpressions)][
let $test := StandardCondition/BooleanExpression[@Op = ('==', '!=')][count(*) = 2][count(LiteralExpression[@Null = true()]) = 1],
$checked := $test/*[not(self::LiteralExpression[@Null = true()])],
$value := if ($test/@Op = '!=') then *[2] else *[3]
return exists($checked) and (
($checked/self::VariableExpression and $value/self::VariableExpression
and lower-case($checked/@Image) = lower-case($value/@Image)
and (
($checked/EmptyReferenceExpression and $value/EmptyReferenceExpression)
or ($checked/ReferenceExpression/ThisVariableExpression and $value/ReferenceExpression/ThisVariableExpression)
or ($checked/ReferenceExpression[not(*)][@Image != ''] and lower-case($checked/ReferenceExpression/@Image) = lower-case($value/ReferenceExpression[not(*)]/@Image))
))
or ($checked/self::MethodCallExpression[@InputParametersSize = 0][not(*[1]/*)]
and $value/self::MethodCallExpression[@InputParametersSize = 0][not(*[1]/*)]
and lower-case($checked/@FullMethodName) = lower-case($value/@FullMethodName))
or ($checked/self::VariableExpression[EmptyReferenceExpression]
and $value[self::MethodCallExpression or self::VariableExpression]/descendant::*
[self::ReferenceExpression[not(*)] or self::VariableExpression[EmptyReferenceExpression][parent::ReferenceExpression]]
[lower-case(@Image) = lower-case($checked/@Image)]
[every $step in ancestor-or-self::*[. >> $value] satisfies
($step/self::ReferenceExpression and not($step/preceding-sibling::*))
or (($step/self::MethodCallExpression or $step/self::VariableExpression) and $step/parent::ReferenceExpression)])
)
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an if statement that only checks a variable for null and then
makes one call or one read on it: the condition is variable != null
or null != variable and nothing else; there is no else and no else
if; and the block holds one statement, which is either a call whose
chain starts at the variable (obj.doSomething(), obj.Owner.m(), or
obj.a().b()) or the declaration of a local variable whose value is
such a call or a field read on the variable (String n = obj.Name;).
Such a statement is written obj?.doSomething(); or
String n = obj?.Name;. Not reported, because ?. cannot be used or
would change what the code does: more logic in the condition, an else,
more than one statement, an assignment (obj.Name = x, or x = obj.Name,
which would set x to null), an increment or decrement, a return, the
variable passed as or inside an argument (foo(obj.Name) would still
call foo), a SOQL bind (:obj.Id), addError(), a static call or field
(String.format(), Integer.MAX_VALUE), and a namespace, class, or
Trigger reference (Trigger.new, Page.MyPage). Names are compared
without regard to case, so a local variable named like the class it
calls (string and String.format()) is reported.
Adapted from PreferSafeNavigationOperator of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="PreferSafeNavigationOperator"
language="apex"
message="Replace this null check with the safe navigation operator (?.)"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An if statement that only checks an object for null, and then makes one call on it or reads one value from it, is written as one statement with the safe navigation operator (?.). The statement says the same thing without the extra block and its indentation. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//IfBlockStatement[count(../*) = 1][
let $name := StandardCondition/BooleanExpression[@Op = '!='][count(*) = 2][LiteralExpression[@Null = true()]]/VariableExpression[EmptyReferenceExpression]/lower-case(@Image),
$statement := BlockStatement[count(*) = 1]/*,
$use := ($statement/self::ExpressionStatement/MethodCallExpression
| $statement/self::VariableDeclarationStatements[count(VariableDeclaration) = 1]/VariableDeclaration[count(*) = 2]/*[1][self::MethodCallExpression or self::VariableExpression])
return exists($name) and exists($use)
and not($use/self::MethodCallExpression[lower-case(@MethodName) = 'adderror'])
and exists($use/descendant::*
[self::ReferenceExpression[not(*)] or self::VariableExpression[EmptyReferenceExpression][parent::ReferenceExpression]]
[lower-case(@Image) = $name]
[every $step in ancestor-or-self::*[. >> $use] satisfies
($step/self::ReferenceExpression and not($step/preceding-sibling::*))
or (($step/self::MethodCallExpression or $step/self::VariableExpression) and $step/parent::ReferenceExpression)])
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a method named getX, with no parameters and a return type,
whose body is one statement that returns x or this.x, where x is a
field or a property declared in the same class, static when the
method is static. Names are compared without regard to case, so
getName() that returns NAME is reported. Such a method is written as
a property, public String x { get; private set; }, and code that
calls getX() reads x instead. A Visualforce page binds to the
property with the same expression, {!x}. Not reported, because a
property cannot take the method's place: a method that is override,
virtual, abstract, or global; a method with an annotation other than
@TestVisible or @SuppressWarnings (an @AuraEnabled or @RemoteAction
method is called by name from the client, and an @HttpGet method is
a REST resource); and any method of a class that implements an
interface, because the method may be one the interface requires,
and an interface cannot declare a property. A field inherited from a
superclass, or a static field of an outer class, is not read.
Adapted from AvoidTrivialPropertyGetters of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="AvoidTrivialPropertyGetters"
language="apex"
message="Replace this getter, which only returns a field, with a property"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A method named getX whose body only returns the field x is written as a property instead, such as public String x { get; private set; }. The property keeps the value and the way it is read in one declaration. A Visualforce page binds to the property with the same expression, {!x}, and code that called the method reads the property. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Method[@Arity = 0][@Constructor = false()][lower-case(@ReturnType) != 'void'][matches(@Image, '^get.', 'i')]
[parent::UserClass[not(@InterfaceNames != '')]]
[not(ModifierNode[@Override = true() or @Virtual = true() or @Abstract = true() or @Global = true()])]
[not(ModifierNode/Annotation[not(lower-case(@Name) = ('testvisible', 'suppresswarnings'))])]
[
let $field := lower-case(substring(@Image, 4)),
$static := ModifierNode/@Static
return BlockStatement[count(*) = 1]/ReturnStatement/VariableExpression[EmptyReferenceExpression or ReferenceExpression/ThisVariableExpression][lower-case(@Image) = $field]
and parent::UserClass/(Field | Property/Field)[lower-case(@Image) = $field][ModifierNode/@Static = $static]
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an if statement whose block holds only another if statement,
when neither has an else or an else if after it. The two are written
as one if statement that joins the conditions with &&, which runs the
same code in the same cases. The outer statement may be the last
branch of an else if chain with no else: if (a) { ... } else if (b)
{ if (c) { ... } } is reported at the else if line, and is written
else if (b && c). A first branch followed by else or else if is not
reported, because joining its conditions would send the case where
the outer condition holds and the inner one does not to the next
branch. An if statement that is reported can also be the inner one of
another report, so three nested if statements give two reports.
Nesting is read whether or not the blocks have braces.
Adapted from CombineNestedIfStatements of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="CombineNestedIfStatements"
language="apex"
message="Join this if statement and the if statement inside it into one, with && between the conditions"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An if statement whose only statement is another if statement, when neither has an else, is written as one if statement that joins the two conditions with &&. One condition is easier to read than two levels of nesting, and the code runs in the same cases. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//IfBlockStatement[not(following-sibling::*)][BlockStatement[count(*) = 1]/IfElseBlockStatement[count(*) = 1]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a local variable that is given a value and returned in the
very next statement: a declaration with an initialiser followed by
return of that variable, or an assignment with = to a local variable
of the method followed by return of it. The returned expression must
be the bare variable; this.name and other.name are not read as the
local. A declaration without an initialiser, a compound assignment
such as +=, and a variable that a statement between the two uses are
not reported. The rule does not look at a finally block that reads
the variable after the return.
Adapted from NoUnnecessaryReturnVariables of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="NoUnnecessaryReturnVariables"
language="apex"
message="Return this value directly instead of storing it in a variable that is only returned"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A value that is assigned to a local variable and returned in the next statement should be returned directly. The variable adds a line to read and nothing else. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>5</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//VariableDeclarationStatements[following-sibling::*[1][self::ReturnStatement]]
/VariableDeclaration[count(*) = 2]
[lower-case(@Image) = ../following-sibling::*[1]/VariableExpression[not(*[not(self::EmptyReferenceExpression)])]/lower-case(@Image)]
| //ExpressionStatement[following-sibling::*[1][self::ReturnStatement]]
[AssignmentExpression[@Op = '=']/*[1][self::VariableExpression][not(*[not(self::EmptyReferenceExpression)])]
[lower-case(@Image) = ../../following-sibling::*[1]/VariableExpression[not(*[not(self::EmptyReferenceExpression)])]/lower-case(@Image)]
[lower-case(@Image) = ancestor::Method[1]//VariableDeclaration/lower-case(@Image)]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a statement that starts on the line where the statement before
it in the same block ends, and the first statement of the block of an
if, else, for, while, or do statement when it is on the line of the
opening brace, such as if (x > 0) { a = 5; }. Prettier formats neither
form, so a report usually means a prettier-ignore comment kept the
line on purpose. From the CodeScan rule
sf:StatementsShouldBeOnSeparateLines.
-->
<rule
name="StatementsShouldBeOnSeparateLines"
language="apex"
message="Put each statement on its own line"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Each statement is on its own line. Line coverage counts a line once, so a line with two statements, or a one-line if block, counts as covered when only part of it ran. Prettier never writes these forms.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//BlockStatement/*[preceding-sibling::*[1]/@EndLine = @BeginLine]
|
//BlockStatement[parent::IfBlockStatement or parent::IfElseBlockStatement or parent::ForLoopStatement or parent::ForEachStatement or parent::WhileLoopStatement or parent::DoLoopStatement]
/*[1][@BeginLine = ../@BeginLine]
]]></value>
</property>
</properties>
</rule>
<!--
Reports the negation of a single comparison. A negated expression with
and-and or or-or in it is not reported. The name ends in Apex because
PMD ships a Java rule named LogicInversion. From the CodeScan rule
sf:LogicInversion.
-->
<rule
name="LogicInversionApex"
language="apex"
message="Use the opposite comparison operator in place of negating the comparison"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A negated comparison is written with the opposite operator: x != y, not !(x == y), and x >= y, not !(x < y).</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//PrefixExpression[@Op = '!']/BooleanExpression[@Op = ('==', '!=', '<', '>', '<=', '>=', '===', '!==')]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an if with one branch that only returns true or false,
followed by an else, or by a return statement, that only returns the
other. The name ends in Apex because PMD ships a Java rule named
SimplifyBooleanReturns. From the CodeScan rule
sf:SimplifyBooleanReturns.
-->
<rule
name="SimplifyBooleanReturnsApex"
language="apex"
message="Return the condition itself in place of an if statement that returns true or false"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An if statement that only returns true or false is written as a return of its condition: return x > 0, not if (x > 0) { return true; } return false.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//IfElseBlockStatement[@ElseStatement = true()][count(IfBlockStatement) = 1]
[IfBlockStatement/BlockStatement[count(*) = 1]/ReturnStatement/LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]]
[BlockStatement[count(*) = 1]/ReturnStatement/LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]]
|
//IfElseBlockStatement[@ElseStatement = false()][count(IfBlockStatement) = 1]
[IfBlockStatement/BlockStatement[count(*) = 1]/ReturnStatement/LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]]
[following-sibling::*[1][self::ReturnStatement[LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]]]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a ternary expression with true or false as one of its results.
The name ends in Apex because PMD ships a Java rule named
SimplifiedTernary. From the CodeScan rule sf:SimplifiedTernary.
-->
<rule
name="SimplifiedTernaryApex"
language="apex"
message="Write this ternary expression with and-and or or-or"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A ternary expression with a Boolean literal as a result is written with && or ||: flag || other, not flag ? true : other.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//TernaryExpression[LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]][not(LiteralExpression[@LiteralType = 'NULL'])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports o != null and-and o instanceof T, and o == null or-or !(o
instanceof T), for the same variable. The name ends in Apex because
PMD ships a Java rule named SimplifyConditional. From the CodeScan
rule sf:SimplifyConditional.
-->
<rule
name="SimplifyConditionalApex"
language="apex"
message="Remove this null check - instanceof is already false for null"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A null check next to an instanceof test is removed, because instanceof is false for null: o instanceof Account, not o != null && o instanceof Account.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//BooleanExpression[@Op = '&&']
[BooleanExpression[@Op = '!='][LiteralExpression[@LiteralType = 'NULL']]/VariableExpression/@Image = InstanceOfExpression/VariableExpression/@Image]
|
//BooleanExpression[@Op = '||']
[BooleanExpression[@Op = '=='][LiteralExpression[@LiteralType = 'NULL']]/VariableExpression/@Image = PrefixExpression[@Op = '!']/InstanceOfExpression/VariableExpression/@Image]
]]></value>
</property>
</properties>
</rule>
<!--
Reports length() or isEmpty() called on the result of trim(), and the
result of trim() compared with an empty string. Keeping the trimmed
value in a variable is not reported. The name ends in Apex because PMD
ships a Java rule named InefficientEmptyStringCheck. From the CodeScan
rule sf:InefficientEmptyStringCheck.
-->
<rule
name="InefficientEmptyStringCheckApex"
language="apex"
message="Use String.isBlank() in place of trim() to test for a blank string"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test for a blank string uses String.isBlank(s), not s.trim().length() == 0, s.trim().isEmpty(), or s.trim() == '', which build a new string only to test it.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@MethodName) = ('length', 'isempty')][ReferenceExpression/MethodCallExpression[lower-case(@MethodName) = 'trim']]
|
//BooleanExpression[@Op = ('==', '!=')][MethodCallExpression[lower-case(@MethodName) = 'trim']][LiteralExpression[@String = true()][@Image = '']]
]]></value>
</property>
</properties>
</rule>
<!--
Reports size() compared with 0 or 1 in a way that tests for an empty
or a non-empty collection. A Blob, which has size() and no isEmpty(),
is not reported when the class declares it. PMD reads no types, so a
size() method of another class can be reported. The name ends in Apex
because PMD ships a Java rule named UseCollectionIsEmpty. From the
CodeScan rule sf:UseCollectionIsEmpty.
-->
<rule
name="UseCollectionIsEmptyApex"
language="apex"
message="Use isEmpty() in place of comparing size() with zero"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A collection is tested with isEmpty(): xs.isEmpty(), not xs.size() == 0, and !xs.isEmpty(), not xs.size() > 0. It states the question that the code asks.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//BooleanExpression[
(*[1][self::MethodCallExpression[lower-case(@MethodName) = 'size'][@InputParametersSize = 0]] and *[2][self::LiteralExpression]
and ((@Op = ('==', '!=', '>', '<=') and *[2]/@Image = '0') or (@Op = ('<', '>=') and *[2]/@Image = '1')))
or
(*[2][self::MethodCallExpression[lower-case(@MethodName) = 'size'][@InputParametersSize = 0]] and *[1][self::LiteralExpression]
and ((@Op = ('==', '!=', '<', '>=') and *[1]/@Image = '0') or (@Op = ('>', '<=') and *[1]/@Image = '1')))
][
let $v := MethodCallExpression/ReferenceExpression/@Image
return not(ancestor::UserClass//(Field | VariableDeclaration | Parameter)[@Image = $v][lower-case(@Type) = 'blob'])
]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX HARD-CODED VALUES -->
<!-- ============================================================ -->
<!--
Reports a string literal that is an IPv4 address, alone or with a
port, or a URL whose host is one. An address inside other text is not
reported, so that a version number in a sentence or in a path is not
taken for one. A string that is nothing but a four-part version
number, such as '1.0.0.0', cannot be told from an address and is
reported.
-->
<rule
name="AvoidHardcodedIpAddressApex"
language="apex"
message="Do not hard-code an IP address - read it from configuration"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A string literal that is an IPv4 address, or a URL whose host is one, ties the code to one network address. Read the address from a Named Credential, a custom metadata record, or a custom setting.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//LiteralExpression[@String = true()][matches(@Image, '(^|://)((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])($|[:/?#])')]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a string literal that holds an email address, alone or inside
other text, outside a class with the IsTest annotation. A test class
is not read, because a test creates users with an address. From the
CodeScan rule sf:EmailAddressInLiteral.
-->
<rule
name="EmailAddressInLiteral"
language="apex"
message="Do not hard-code an email address - read it from configuration, or address a user or a group"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A string literal that holds an email address sends mail to one fixed person, or puts a real address in the repository. Read the address from a custom metadata record or a custom setting, or send to a User, a Contact, or a Group. A test class is not read.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//LiteralExpression[@String = true()]
[matches(@Image, '[A-Za-z0-9._%+\-]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)*\.[A-Za-z]{2,}')]
[not(ancestor::UserClass/ModifierNode[@Test = true()])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a string literal that holds the absolute URL of a Salesforce
org: a My Domain, Lightning, Visualforce, Sites, or Experience Cloud
host, an instance host, or one of the two login hosts. The URL is found
anywhere in the string. The allowedSubdomains property holds the
subdomains of salesforce.com that may be written out, and a string that
links to one of them is not reported. It starts with the public
Salesforce websites, which are the same for every org. An organisation
adds its own: login allows https://login.salesforce.com, and acme.my
allows https://acme.my.salesforce.com.
-->
<rule
name="AvoidAbsoluteSalesforceUrl"
language="apex"
message="Do not hard-code a Salesforce URL - use a relative URL, or as a last resort build it from the org domain"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A string literal that holds the absolute URL of a Salesforce org is right in one org only, because every sandbox and every production org has a host name of its own. Use a relative URL. Where a URL has to be absolute, build it from URL.getOrgDomainUrl() as a last resort. The allowedSubdomains property holds the subdomains of salesforce.com that may be written out, and each organisation adds its own.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//LiteralExpression[@String = true()][matches(@Image, '(^|[^a-z0-9@.-])([a-z0-9-]+\.)+((salesforce|force|visualforce|salesforce-sites|salesforce-setup|cloudforce)\.com|my\.site\.com)', 'i')][not(some $s in $allowedSubdomains satisfies matches(@Image, concat('(^|[^a-z0-9@.-])', $s, '\.salesforce\.com'), 'i'))]
]]></value>
</property>
<property
name="allowedSubdomains"
type="List[String]"
value="www,help,developer,trailhead,status,trust,appexchange"
description="The subdomains of salesforce.com whose URLs may be written out, each as the part of the host before .salesforce.com" />
</properties>
</rule>
<!--
Reports a numeric literal (an Integer, Long, Decimal, or Double) whose
value is not in the allowedNumbers property, which holds 0, 1, and -1
by default. A minus sign written before the number is part of its
value, and the values are compared as numbers, so 1.0 and 1L match 1.
These are not reported: a number in the index of a list (xs[2], or
xs[xs.size() - 2]), in the condition of a for, while, or do loop, in
the initialiser of a static final field or in its assignment in a
static initialiser block, in a when value of a switch statement (which
must be a literal), or in a comparison whose other side is an allowed
number. A test class, an inner class of a test class included, is not
read. A number inside a SOQL query is not read.
Adapted from AvoidMagicNumbers of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="AvoidMagicNumbers"
language="apex"
message="Replace this number with a constant whose name says what the number means"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A number written in the code says nothing about what it means, and when the same value is needed in two places, the two copies can drift apart. A number is declared once, as a static final constant whose name says what it means, and the code uses the constant. The allowedNumbers property lists the numbers that may be written in the code, and it holds 0, 1, and -1 by default. A list index, a loop condition, a when value of a switch statement, and a test class are not reported. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
let $allowed := for $number in $allowedNumbers return number(normalize-space($number)),
$value := function($node) {
if ($node/self::PrefixExpression[@Op = '-']) then -number($node/LiteralExpression[@LiteralType = ('INTEGER', 'LONG', 'DECIMAL', 'DOUBLE')]/@Image)
else number($node/self::LiteralExpression[@LiteralType = ('INTEGER', 'LONG', 'DECIMAL', 'DOUBLE')]/@Image)
}
return //LiteralExpression[@LiteralType = ('INTEGER', 'LONG', 'DECIMAL', 'DOUBLE')]
[not(ancestor::UserClass/ModifierNode[@Test = true()])]
[not($value((parent::PrefixExpression[@Op = '-'], .)[1]) = $allowed)]
[not(ancestor-or-self::*[parent::ArrayLoadExpression or parent::ArrayStoreExpression][preceding-sibling::*])]
[not(ancestor::StandardCondition[parent::ForLoopStatement or parent::WhileLoopStatement or parent::DoLoopStatement])]
[not(ancestor::FieldDeclarationStatements[ModifierNode[@Static = true() and @Final = true()]])]
[not(ancestor::AssignmentExpression[ancestor::Method[1][@StaticInitializer = true()]]/*[1][self::VariableExpression[not(ReferenceExpression[@Image != ''])]]/lower-case(@Image)
= ancestor::UserClass[1]/FieldDeclarationStatements[ModifierNode[@Static = true() and @Final = true()]]/FieldDeclaration/lower-case(@Name))]
[not(ancestor::LiteralCase)]
[not(
let $operand := (parent::PrefixExpression[@Op = '-'], .)[1]
return $operand/parent::BooleanExpression[@Op = ('==', '!=', '<>', '===', '!==', '<', '<=', '>', '>=')]
and (some $other in $operand/../*[not(. is $operand)] satisfies $value($other) = $allowed)
)]
]]></value>
</property>
<property
name="allowedNumbers"
type="List[String]"
value="0,1,-1"
description="The numbers that may be written in the code, compared as numbers, with a minus sign for a negative number" />
</properties>
</rule>
<!--
Code Analyzer's AvoidUnauthorizedApiSessionIdInApex reports a string
that holds {!API.Session_ID}, without the dollar sign, and not the usual
{!$Api.Session_ID}. This rule reports the dollar form, in any case, so
that the two never report the same string. Like Code Analyzer's rule, it
skips a test class and a test method.
-->
<rule
name="AvoidApiSessionIdInApexString"
language="apex"
message="Do not write the session Id into a string - call Salesforce through a Named Credential"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A session Id is a credential. A string in Apex that holds the merge field {!$Api.Session_ID}, for a page or an email to fill in, hands it out. A call to Salesforce goes through a Named Credential.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//LiteralExpression[@String = true()][contains(upper-case(@Image), '{!$API.SESSION_ID}')][not(ancestor::UserClass/ModifierNode[@Test = true()])][not(ancestor::Method/ModifierNode[@Test = true()])]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX COOKIES AND CRYPTOGRAPHY -->
<!-- ============================================================ -->
<!--
Reports a Cookie created with the literal false as its fifth argument,
isSecure, with or without the sixth argument, SameSite. A value passed
in a variable is not followed.
-->
<rule
name="AvoidInsecureCookies"
language="apex"
message="Create this cookie with isSecure set to true"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A cookie created with isSecure set to false is also sent over a connection that is not encrypted, where it can be read. Create every cookie with isSecure set to true.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//NewObjectExpression[lower-case(@Type) = ('cookie', 'system.cookie')][*[5][self::LiteralExpression[@LiteralType = 'FALSE']]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a call to a digest, MAC, or signature method of the Crypto class
that names MD5 or SHA-1 in a string literal. In Apex the algorithm
name RSA is another name for RSA-SHA1. An algorithm name passed in a
variable or a constant is not followed.
-->
<rule
name="AvoidWeakCryptoAlgorithms"
language="apex"
message="Use SHA-256 or a stronger algorithm - MD5 and SHA-1 are not collision resistant"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>MD5 and SHA-1 are not collision resistant, and SHA-1 is being retired for every use. Name an algorithm of the SHA-2 family for a digest, a message authentication code, or a signature.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[matches(@FullMethodName, '^(system\.)?crypto\.(generateDigest|generateMac|sign|signWithCertificate|signXml|verify|verifyHmac|verifyWithCertificate)$', 'i')][LiteralExpression[@String = true()][matches(@Image, '^(md5|sha-?1|hmacmd5|hmacsha-?1|rsa|rsa-sha1)$', 'i')]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports Crypto.encrypt, Crypto.encryptWithManagedIV, Crypto.decrypt,
and Crypto.decryptWithManagedIV. It marks a place for a security
review, not a defect: AvoidWeakCryptoAlgorithms and the stock rule
ApexBadCrypto report defects. From the CodeScan rule
sf:HotspotEncrypt.
-->
<rule
name="HotspotEncrypt"
language="apex"
message="Encryption or decryption here needs a security review of where its key comes from and how it is kept"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every call that encrypts or decrypts with a key is reviewed for where the key comes from and how it is kept. A reviewer accepts each call once the key is generated or read from a store that the platform protects.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[matches(lower-case(@FullMethodName), '^(system\.)?crypto\.(en|de)crypt(withmanagediv)?$')]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX UNTRUSTED INPUT -->
<!-- ============================================================ -->
<!--
Reports a call to get with the literal Referer, in any case, such as
ApexPages.currentPage().getHeaders().get('Referer') or
RestContext.request.headers.get('Referer'). A header name passed in a
variable is not followed. From the CodeScan rule sf:AvoidHttpReferer.
-->
<rule
name="AvoidHttpReferer"
language="apex"
message="Do not base a security decision on the Referer header - it is often missing and does not say who sent the request"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>No security decision rests on the HTTP Referer header. Browsers and network devices often leave it out or strip it for privacy, and it says where a request came from, not who sent it. A reviewer accepts a read that makes no such decision.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@MethodName) = 'get'][LiteralExpression[lower-case(@Image) = 'referer']]
]]></value>
</property>
</properties>
</rule>
<!--
Reports JSON.deserialize, JSON.deserializeStrict,
JSON.deserializeUntyped, and JSON.createParser, the ways that Apex
reads JSON text. It marks a place for a security review, not a defect.
From the CodeScan rule sf:HotspotDeserializeJson.
-->
<rule
name="HotspotDeserializeJson"
language="apex"
message="Reading JSON here needs a security review of where the text comes from and how it is checked"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every place that reads JSON is reviewed for where the text comes from and whether it is checked before it is used. A reviewer accepts each one once the source is trusted or the result is validated.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[matches(lower-case(@FullMethodName), '^(system\.)?json\.(deserialize(untyped|strict)?|createparser)$')]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX PERFORMANCE -->
<!-- ============================================================ -->
<!--
Reports getDescribe(), fields.getMap(), and fieldSets.getMap() in the
body of a loop, when the call uses no variable that the loop declares.
A call on the loop variable, such as field.getDescribe() in a loop
over fields, differs on each pass and is not reported, and neither is
a call in the expression a for-each loop iterates over, which runs
once. Schema.getGlobalDescribe() and Schema.describeSObjects() in a
loop are left to PMD's OperationWithHighCostInLoop.
-->
<rule
name="AvoidDescribeInLoops"
language="apex"
message="Move this describe call out of the loop - its result is the same on every pass"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A describe call in the body of a loop is run again on every pass. When the call does not use a variable of the loop, its result is the same each time: make the call once, before the loop.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@MethodName) = 'getdescribe' or matches(@FullMethodName, '(^|\.)(fields|fieldSets)\.getMap$', 'i')][ancestor::BlockStatement/parent::*[self::ForEachStatement or self::ForLoopStatement or self::WhileLoopStatement or self::DoLoopStatement]][not((descendant::ReferenceExpression | descendant::VariableExpression)/lower-case(@Image) = ancestor::*[self::ForEachStatement or self::ForLoopStatement or self::WhileLoopStatement or self::DoLoopStatement]//VariableDeclaration/lower-case(@Image))]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a call, in the body of a loop, to a method of the same file that
runs DML: an insert, update, upsert, delete, undelete, or merge
statement, or the Database method of the same name. A loop of any kind
is read (for, for-each, while, and do), and so is a loop body with no
braces; the collection that a for-each loop iterates over is not. The
call is reported, not the loop, so a call in a nested loop is reported
once. Methods are matched by name, without regard to case, so a call to
another object's method of the same name is reported too. Only one
step is followed: a method that calls the DML method is not read, and
neither is a method in another file. DML in the loop body itself is
left to PMD's OperationWithLimitsInLoop.
Adapted from DmlInLoopAcrossMethods of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="DmlInLoopAcrossMethods"
language="apex"
message="This loop calls a method that runs DML. Collect the records in a list in the loop, and run the DML once, after the loop"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A loop that calls a method which runs DML runs that DML once on every pass, and each statement counts against the limit on DML statements in a transaction. Collect the records in the loop, and run one DML statement after it. The rule reads methods of the same file only, and it matches them by name. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
let $dmlMethods := (//Method[
.//(DmlInsertStatement | DmlUpdateStatement | DmlUpsertStatement | DmlDeleteStatement | DmlUndeleteStatement | DmlMergeStatement)
or .//MethodCallExpression[lower-case(@FullMethodName) = ('database.insert', 'database.update', 'database.upsert', 'database.delete', 'database.undelete', 'database.merge')]
]/lower-case(@Image))
return //MethodCallExpression[lower-case(@MethodName) = $dmlMethods]
[ancestor::BlockStatement/parent::*[self::ForEachStatement or self::ForLoopStatement or self::WhileLoopStatement or self::DoLoopStatement]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a SOQL query whose WHERE clause has a filter that cannot use an
index: a negative filter (!=, <>, EXCLUDES, or NOT as a logical
modifier: NOT IN, NOT (...), NOT Field LIKE), or a LIKE whose pattern
starts with a wildcard (% or _). The query planner cannot use an index
for either, so the filter reads the whole table. A query written as a
string is read too: any string literal that starts with SELECT and
holds FROM, wherever it sits, so the string assigned to a variable
before the call is read as well as one passed to the call. Of a query
built from several strings, only the piece that starts with SELECT is
read. A LIKE whose pattern is a bind variable is not read, because the
pattern is not in the code.
-->
<rule
name="AvoidNonSelectiveSoqlFilter"
language="apex"
message="Do not use a negative expression or a leading wildcard in a WHERE clause. It cannot use an index, so the query is not selective and reads the whole table"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A WHERE clause with a negative filter (!=, <>, NOT, NOT IN, EXCLUDES), or with a LIKE whose pattern starts with a wildcard, cannot use an index, so the query reads every row of the object, affecting performance and failing on large data volumes. Inline SOQL is read, and so is a query written as a string, wherever the string sits.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//SoqlExpression[matches(replace(@Query, "'[^']*'", "''"), '\sWHERE\s(.*(!=|<>)|(.*[\s(])?NOT\s*[\s(]|.*\sEXCLUDES\s*\()', 'is') or matches(@Query, "\sLIKE\s*'[%_]", 'i')]
|
//LiteralExpression[@String = true()][matches(@Image, '^\s*SELECT\s.*\sFROM\s', 'is')][matches(replace(@Image, "'[^']*'", "''"), '\sWHERE\s(.*(!=|<>)|(.*[\s(])?NOT\s*[\s(]|.*\sEXCLUDES\s*\()', 'is') or matches(@Image, "\sLIKE\s*\\?'[%_]", 'i')]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a call to Pattern.compile() or Pattern.matches(), with or
without the System namespace, whose pattern argument is a string
literal written in the call. The call is not reported in the
initialiser of a static final field, or in a static initialiser block
when the call is part of an assignment to a static final field of the
same class, because there the pattern is compiled once. A pattern held
in a variable or a constant is not read, so a pattern built at run time
is never reported, and neither is a literal joined to another string
with +. The methods of String that take a regular expression (split(),
matches(), replaceAll(), and replaceFirst()) are not read.
Adapted from RegexPatternsMustBeStaticFinal of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="RegexPatternsMustBeStaticFinal"
language="apex"
message="Compile this regular expression once, into a static final Pattern, instead of on every call"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Pattern.compile() and Pattern.matches() compile their regular expression each time they run, and that costs CPU time on every call. A regular expression that is written in the code is compiled once, into a static final Pattern, and the code uses that Pattern. The field also gives the expression a name, and every use of it reads the same definition. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('pattern.compile', 'pattern.matches', 'system.pattern.compile', 'system.pattern.matches')]
[*[2][self::LiteralExpression[@String = true()]]]
[not(ancestor::FieldDeclarationStatements[ModifierNode[@Static = true() and @Final = true()]])]
[not(ancestor::AssignmentExpression[ancestor::Method[1][@StaticInitializer = true()]]/*[1][self::VariableExpression[not(ReferenceExpression[@Image != ''])]]/lower-case(@Image)
= ancestor::UserClass[1]/FieldDeclarationStatements[ModifierNode[@Static = true() and @Final = true()]]/FieldDeclaration/lower-case(@Name))]
]]></value>
</property>
</properties>
</rule>
<!--
Reports size() called directly on an inline query or on the result of
Database.query or Database.queryWithBinds. Records kept in a variable,
which other code can use, are not reported. From the CodeScan rule
sf:AvoidSizeOfSoql.
-->
<rule
name="AvoidSizeOfSoql"
language="apex"
message="Count records with an aggregate query, such as SELECT COUNT(), in place of size() on a query"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A count comes from an aggregate query, such as SELECT COUNT() FROM Account, not from size() on a query that loads every record only to count them.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@MethodName) = 'size'][
ReferenceExpression/SoqlExpression
or ReferenceExpression/MethodCallExpression[lower-case(@FullMethodName) = ('database.query', 'database.querywithbinds')]
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a query in the get accessor of any property, in every class,
and in a get method with no parameters of a class whose name matches
controllerPattern, the Visualforce controllers and extensions. A query
inside an if whose condition checks a value for null is lazy loading
and is not reported. A get method of a selector or a service, which
runs only when it is called, is not reported. The controllerPattern
property should match the patterns of VisualforceControllerNaming.
Adapted from the CodeScan rule sf:AvoidSOQLInControllerGetter, renamed
because it reads every property.
-->
<rule
name="RequireLazyLoadingInGetters"
language="apex"
message="Load this value lazily - query it only when it is null, and keep it, so that it is not queried again on every read"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A getter that queries loads its value lazily: it runs the query only when the stored value is null, and keeps the result. Otherwise every read of a property, and every call of a Visualforce controller getter, runs the query again.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Property/Method[@Arity = 0]//(SoqlExpression | MethodCallExpression[lower-case(@FullMethodName) = ('database.query', 'database.querywithbinds', 'database.countquery')])
[not(ancestor::IfBlockStatement/StandardCondition/BooleanExpression[@Op = '=='][LiteralExpression[@LiteralType = 'NULL']])]
|
//UserClass[matches(@SimpleName, concat('^(', $controllerPattern, ')$'))]/Method[matches(@Image, '^get[A-Z0-9_]')][@Arity = 0]//(SoqlExpression | MethodCallExpression[lower-case(@FullMethodName) = ('database.query', 'database.querywithbinds', 'database.countquery')])
[not(ancestor::IfBlockStatement/StandardCondition/BooleanExpression[@Op = '=='][LiteralExpression[@LiteralType = 'NULL']])]
]]></value>
</property>
<property
name="controllerPattern"
type="String"
value=".*Controller|.*(Extension|Ext)"
description="A regular expression for the names of the Visualforce controllers and extensions whose get methods are read" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX TRIGGERS -->
<!-- ============================================================ -->
<!--
A trigger hands off to a handler class and declares no methods of its
own: a method written in a trigger is logic in the trigger, and it
breaks the one-line trigger that a trigger framework requires, so the
rule is priority 1. Each method is reported at its own line, not at
the trigger's first line. PMD
models the trigger's body as a method too; @RealLoc tells the two
apart. DeclareAccessOnMethods also applies inside a trigger, so a
method there with no access modifier is reported by both rules: run
either rule, or both. A sample rule that checks the whole one-line
trigger of the Trigger Actions Framework is published in its own file,
pmd-trigger-framework-rule.xml; it is not part of this ruleset.
-->
<rule
name="NoMethodsInTriggers"
language="apex"
message="Move this method out of the trigger and into a handler class"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A trigger must not declare methods, with or without an access modifier. Logic belongs in a handler class, where it can be called, tested, and governed like any other code.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserTrigger/Method[@RealLoc = true()]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: APEX TESTING -->
<!-- ============================================================ -->
<!--
The RunRelevantTests test level chooses a test class through a
reference in code, or through the testFor and critical parameters of
its IsTest annotation. This rule asks for one of the two parameters on
every test class, so that no test depends on the analysis alone. Both
parameters compile only at API 66.0 or later, so a test class below
that version has to be raised before it can satisfy the rule. PMD
7.28.0 ships a stock rule for the same check,
ApexUnitTestClassShouldHaveRunRelevantTestsAnnotation, which does not
report a class below API 66.0; this rule reports at every version. An
organisation that is not ready switches this rule off, or lowers its
priority.
-->
<rule
name="RequireTestForOrCritical"
language="apex"
message="Add testFor or critical=true to the IsTest annotation of this test class"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test class names the classes and triggers it tests in the testFor parameter of its IsTest annotation, or is marked with critical=true.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[@Nested = false()]
[ModifierNode/Annotation[lower-case(@Name) = 'istest']]
[Method[ModifierNode/Annotation[lower-case(@Name) = 'istest'] or ModifierNode[@Test = true()]]]
[not(ModifierNode/Annotation[lower-case(@Name) = 'istest']/AnnotationParameter[
(lower-case(@Name) = 'testfor' and string-length(@Value) > 0)
or (lower-case(@Name) = 'critical' and lower-case(@Value) = 'true')
])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a top-level test class, one with an IsTest annotation and at
least one test method, whose own IsTest annotation does not carry
IsParallel=true or IsParallel=false: an explicit IsParallel=false is a
stated choice and passes (David, 2026-10-24). A test
class is not reported when it cannot run in parallel: when the class or
any of its methods has SeeAllData=true; when it calls
Test.getStandardPricebookId(), System.schedule(), or
System.enqueueJob(); or when it creates a ContentNote, User, or
GroupMember record, which the rule reads as a new expression of that
type anywhere in the file. A new User that sets only its Id, as in
System.runAs(new User(Id = UserInfo.getUserId())), refers to an
existing user and does not count. The setup objects that cannot be
mixed with other sObjects in one DML transaction, and so cannot be used
in a parallel test, are read the same way (David, 2026-10-24), from
the list on the page sObjects That Cannot Be Used Together in DML
Operations of the Apex Developer Guide, which says it is not complete.
Only the test class itself is read: a record that a data factory in
another class creates is not seen, so such a class states
IsParallel=false. A
utility class annotated IsTest with no test method is not reported.
Adapted from TestClassIsParallel of sca-extra,
https://github.com/starch-uk/sca-extra, under its MIT licence:
Copyright (c) 2025 Beech Horn
https://github.com/starch-uk/sca-extra/blob/main/LICENSE.md
-->
<rule
name="TestClassIsParallel"
language="apex"
message="State IsParallel in the IsTest annotation of this test class: true if it can run in parallel, false if it cannot"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test class states whether it can run in parallel, with IsParallel=true or IsParallel=false in its IsTest annotation. IsParallel=true runs the class in parallel even when parallel Apex testing is disabled for the org. A class that cannot run in parallel is not reported: one with SeeAllData=true, one that calls Test.getStandardPricebookId(), System.schedule(), or System.enqueueJob(), and one that creates a ContentNote, a User, a GroupMember, or another setup object that cannot be mixed with other sObjects in one DML transaction. Adapted from sca-extra, https://github.com/starch-uk/sca-extra (MIT licence).</description>
<priority>5</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[@Nested = false()]
[ModifierNode/Annotation[lower-case(@Name) = 'istest']]
[Method[ModifierNode[@Test = true()]]]
[not(ModifierNode/Annotation[lower-case(@Name) = 'istest']/AnnotationParameter[lower-case(@Name) = 'isparallel'])]
[not(.//Annotation[lower-case(@Name) = 'istest']/AnnotationParameter[lower-case(@Name) = 'seealldata' and lower-case(@Value) = 'true'])]
[not(.//MethodCallExpression[lower-case(@FullMethodName) = ('test.getstandardpricebookid', 'system.test.getstandardpricebookid', 'system.schedule', 'system.enqueuejob')])]
[not(.//(NewObjectExpression | NewKeyValueObjectExpression)
[replace(lower-case(@Type), '^schema\.', '') = ('contentnote', 'user', 'groupmember', 'authsession', 'contentworkspace', 'fieldpermissions', 'forecastingshare', 'group', 'objectpermissions', 'objectterritory2assignmentrule', 'objectterritory2assignmentruleitem', 'permissionset', 'permissionsetassignment', 'queuesobject', 'ruleterritory2association', 'setupentityaccess', 'territory', 'territory2', 'territory2model', 'userpackagelicense', 'userrole', 'userterritory', 'userterritory2association')]
[not(@ParameterCount = 1 and AssignmentExpression/VariableExpression[lower-case(@Image) = 'id'])])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a call to Test.isRunningTest() outside a class with the IsTest
annotation. A call in a test class, or in a class nested in one, is
not reported, because test code hides nothing from a test. From the
CodeScan rule sf:AvoidUsingTestIsRunningTest.
-->
<rule
name="AvoidUsingTestIsRunningTest"
language="apex"
message="Do not branch on Test.isRunningTest() in production code; use a mock or a test double instead"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Production code does not call Test.isRunningTest(). Code that behaves one way in a test and another way in the org is not tested by its tests. A call in a class with the IsTest annotation is not reported.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('test.isrunningtest', 'system.test.isrunningtest')]
[not(ancestor::UserClass/ModifierNode[@Test = true()])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports System.assert, System.assertEquals, and
System.assertNotEquals. The Assert class, such as Assert.areEqual, is
the newer form. From the CodeScan rule sf:RefactorAssert.
-->
<rule
name="RefactorAssert"
language="apex"
message="Use the Assert class in place of System.assert, System.assertEquals, or System.assertNotEquals"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An assertion uses the Assert class, such as Assert.areEqual or Assert.isTrue, not the older System.assert, System.assertEquals, and System.assertNotEquals.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('system.assert', 'system.assertequals', 'system.assertnotequals')]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a call to a method of the Assert class, or to System.assert,
System.assertEquals, or System.assertNotEquals, outside a class or a
method with the IsTest annotation. From the CodeScan rule
sf:NonUnitTestContainsAsserts.
-->
<rule
name="NonUnitTestContainsAsserts"
language="apex"
message="Move this assertion into a test, or replace it with an exception that the code can catch"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An assertion is only in test code. A failed assertion throws an exception that no code can catch, so production code that checks a condition throws its own exception instead.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[matches(lower-case(@FullMethodName), '^(system\.)?assert\.[a-z]+$|^system\.assert(equals|notequals)?$')]
[not(ancestor::UserClass/ModifierNode[@Test = true()])]
[not(ancestor::Method/ModifierNode[@Test = true()])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a top-level class with the IsTest annotation that has no test
method. A test helper is not reported: a class with a public or global
static method, such as a test data factory, or with any member that
carries the TestVisible annotation. The name ends in Apex because PMD
ships a Java rule named TestClassWithoutTestCases. From the CodeScan
rule sf:TestClassWithoutTestCases.
-->
<rule
name="TestClassWithoutTestCasesApex"
language="apex"
message="Add a test method to this test class, or remove the IsTest annotation if it is not test code"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test class has at least one test method. A class with the IsTest annotation and no test method is not reported when it is a test helper: when it has a public or global static method, such as a test data factory, or a member with the TestVisible annotation.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserClass[@Nested = false()][ModifierNode/@Test = true()]
[not(.//Method/ModifierNode[@Test = true()])]
[not(Method/ModifierNode[@Static = true()][@Public = true() or @Global = true()])]
[not(.//ModifierNode/Annotation[lower-case(@Name) = 'testvisible'])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports a test method that does not call both Test.startTest() and
Test.stopTest(). From the CodeScan rule
sf:UnitTestsShouldIncludeStartTest.
-->
<rule
name="UnitTestsShouldIncludeStartTest"
language="apex"
message="Call Test.startTest() and Test.stopTest() around the code under test in this test method"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test method calls Test.startTest() and Test.stopTest() around the code under test. The code between them gets its own set of governor limits, and asynchronous work runs when Test.stopTest() is called.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Method[ModifierNode/@Test = true()][
not(.//MethodCallExpression[lower-case(@FullMethodName) = ('test.starttest', 'system.test.starttest')])
or not(.//MethodCallExpression[lower-case(@FullMethodName) = ('test.stoptest', 'system.test.stoptest')])
]
]]></value>
</property>
</properties>
</rule>
<!--
Reports an assertion whose result is fixed by the values true, false,
or null written into it: Assert.isTrue or Assert.isFalse on a Boolean
literal, Assert.isNull or Assert.isNotNull on null, and
Assert.areEqual or Assert.areNotEqual with true, false, or null on
both sides. Every other value written into an assertion is the job of
MisuseOfAssertClass. The name ends in Apex because PMD ships a Java
rule named UnnecessaryBooleanAssertion. From the CodeScan rule
sf:UnnecessaryBooleanAssertion.
-->
<rule
name="UnnecessaryBooleanAssertionApex"
language="apex"
message="This assertion checks only true, false, or null, so its result is fixed; assert a value that the code under test produced"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An assertion checks a value that the code under test produced. An assertion whose result is fixed by true, false, or null written into it, such as Assert.isTrue(true) or Assert.isNull(null), checks nothing.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.istrue', 'assert.isfalse', 'system.assert.istrue', 'system.assert.isfalse', 'system.assert')]
[*[2][self::LiteralExpression[@LiteralType = ('TRUE', 'FALSE')] or self::PrefixExpression[@Op = '!'][LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]]]]
|
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.isnull', 'assert.isnotnull', 'system.assert.isnull', 'system.assert.isnotnull')]
[*[2][self::LiteralExpression[@LiteralType = 'NULL']]]
|
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.areequal', 'assert.arenotequal', 'system.assert.areequal', 'system.assert.arenotequal', 'system.assertequals', 'system.assertnotequals')]
[*[2][self::LiteralExpression[@LiteralType = ('TRUE', 'FALSE', 'NULL')]]]
[*[3][self::LiteralExpression[@LiteralType = ('TRUE', 'FALSE', 'NULL')]]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports Assert.isNull or Assert.isNotNull on a new instance, a string,
or a number, and Assert.areEqual or Assert.areNotEqual with a literal
on both sides. A new instance as the expected value of Assert.areEqual
is a correct test and is not reported. Assertions on true, false, and
null alone are the job of UnnecessaryBooleanAssertionApex, so one
mistake gets one report. From the CodeScan rule
sf:MisuseOfAssertClass.
-->
<rule
name="MisuseOfAssertClass"
language="apex"
message="This assertion compares values written into it, so its result is fixed; assert a value that the code under test produced"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An assertion checks a value that the code under test produced, never only values written into the assertion. Assert.isNotNull on a new instance or a literal cannot fail, and Assert.areEqual on two literals cannot change.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.isnull', 'assert.isnotnull', 'system.assert.isnull', 'system.assert.isnotnull')]
[*[2][starts-with(name(), 'New') or self::LiteralExpression[not(@LiteralType = ('TRUE', 'FALSE', 'NULL'))]]]
|
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.areequal', 'assert.arenotequal', 'system.assert.areequal', 'system.assert.arenotequal', 'system.assertequals', 'system.assertnotequals')]
[*[2][self::LiteralExpression] and *[3][self::LiteralExpression]]
[not(*[2][@LiteralType = ('TRUE', 'FALSE', 'NULL')] and *[3][@LiteralType = ('TRUE', 'FALSE', 'NULL')])]
]]></value>
</property>
</properties>
</rule>
<!--
Reports Assert.isTrue, Assert.isFalse, or System.assert on an == or !=
comparison. A comparison with null is not reported, because
Assert.isNull and Assert.isNotNull fit it. From the CodeScan rule
sf:UseAreEqualInsteadOfAssertBoolean.
-->
<rule
name="UseAreEqualInsteadOfAssertBoolean"
language="apex"
message="Use Assert.areEqual or Assert.areNotEqual in place of an == or != comparison in a Boolean assertion"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test compares two values with Assert.areEqual or Assert.areNotEqual, so that a failure shows both values, not with == or != inside Assert.isTrue or Assert.isFalse.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.istrue', 'assert.isfalse', 'system.assert.istrue', 'system.assert.isfalse', 'system.assert')]
[*[2][self::BooleanExpression[@Op = ('==', '!=')][not(LiteralExpression[@LiteralType = 'NULL'])]]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports Assert.isTrue, Assert.isFalse, or System.assert on an equals()
call. equalsIgnoreCase() is not reported, because Assert.areEqual
compares case. From the CodeScan rule sf:UseAreEqualInsteadOfIsTrue.
-->
<rule
name="UseAreEqualInsteadOfIsTrue"
language="apex"
message="Use Assert.areEqual or Assert.areNotEqual in place of an equals() call in a Boolean assertion"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test compares two values with Assert.areEqual or Assert.areNotEqual, so that a failure shows both values, not with an equals() call inside Assert.isTrue or Assert.isFalse.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.istrue', 'assert.isfalse', 'system.assert.istrue', 'system.assert.isfalse', 'system.assert')]
[*[2][self::MethodCallExpression[lower-case(@MethodName) = 'equals'][@InputParametersSize = 1]]]
]]></value>
</property>
</properties>
</rule>
<!--
Reports Assert.areEqual, Assert.areNotEqual, System.assertEquals, or
System.assertNotEquals with a Boolean literal on one side. When both
sides are true, false, or null, the assertion is fixed, and
UnnecessaryBooleanAssertionApex reports it instead. From the CodeScan
rule sf:UseIsTrueInsteadOfAreEqual.
-->
<rule
name="UseIsTrueInsteadOfAreEqual"
language="apex"
message="Use Assert.isTrue or Assert.isFalse in place of an equality assertion against a Boolean literal"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A test checks a Boolean value with Assert.isTrue or Assert.isFalse, not with Assert.areEqual and a Boolean literal.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//MethodCallExpression[lower-case(@FullMethodName) = ('assert.areequal', 'assert.arenotequal', 'system.assert.areequal', 'system.assert.arenotequal', 'system.assertequals', 'system.assertnotequals')]
[*[position() = (2, 3)][self::LiteralExpression[@LiteralType = ('TRUE', 'FALSE')]]]
[not(*[2][self::LiteralExpression[@LiteralType = ('TRUE', 'FALSE', 'NULL')]] and *[3][self::LiteralExpression[@LiteralType = ('TRUE', 'FALSE', 'NULL')]])]
]]></value>
</property>
</properties>
</rule>
</ruleset>
The custom rules for Visualforce pages and components, and for the markup of Lightning web components and Aura components.
Markup
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="DevSecOps Reference Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
Custom PMD rules for Visualforce pages and components and for the markup
of Lightning web components and Aura components, written in XPath. The
other custom rules are in pmd-custom-rules-apex.xml,
pmd-custom-rules-metadata.xml, pmd-custom-rules-flow.xml, and, for
JavaScript, pmd-custom-rules-javascript.xml. The stock PMD rules this
reference adopts are in pmd-stock-rules.xml and, for JavaScript, in
pmd-stock-rules-javascript.xml. Adjust priorities and suppressions to
match your organisation's risk posture and coding standards.
</description>
<!--
Rule messages contain no apostrophes. PMD formats a message with Java's
MessageFormat, which treats a single apostrophe as a quoting character
and drops it, so "class's" would be printed as "classs".
-->
<exclude-pattern>.*/.sfdx/.*</exclude-pattern>
<exclude-pattern>.*/.sf/.*</exclude-pattern>
<exclude-pattern>.*/node_modules/.*</exclude-pattern>
<!-- ============================================================ -->
<!-- CUSTOM RULES: ACCESSIBILITY -->
<!-- ============================================================ -->
<!--
Images and buttons. An image with no alt attribute fails WCAG 1.1.1
(Level A), so the rule blocks the change. An empty alt marks a
decorative image (WCAG technique H67), so it is reported for the
reviewer to confirm and does not block. A button or an image-map link
has no other name, so an empty name blocks as a missing one does. A
merge expression or a bound property counts as text. One PMD rule has
one priority and one language, so each check is a rule for Visualforce
and a rule for HTML, Lightning web component, and Aura markup.
-->
<rule
name="ImageWithoutAltVf"
language="visualforce"
message="Add an alt attribute to this image: text that says what it shows, or an empty alt if it is decorative"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An img or apex:image element has an alt attribute. The text says what the image shows, or the attribute is empty when the image is decorative.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = ('img', 'apex:image')][not(Attribute[lower-case(@Name) = 'alt'])]
]]></value>
</property>
</properties>
</rule>
<rule
name="ImageWithEmptyAltVf"
language="visualforce"
message="An empty alt hides this image from screen readers: confirm that it is decorative, or describe it"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An img or apex:image element whose alt attribute is empty or blank is hidden from assistive technology, which is right only for a decorative image. A merge expression counts as text.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = ('img', 'apex:image')][Attribute[lower-case(@Name) = 'alt'][not(AttributeValue/*[not(self::Text) or normalize-space(@Image) != ''])]]
]]></value>
</property>
</properties>
</rule>
<rule
name="ButtonWithoutNameVf"
language="visualforce"
message="Give this image button or image-map link a name that a screen reader can announce: an alt, aria-label, or aria-labelledby attribute with text"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An image button, an input of type image or an apex:commandButton with an image, and an image-map link, an area with an href, have a name: an alt attribute with text, or an aria-label or aria-labelledby attribute with text. A merge expression counts as text.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[
(lower-case(@Name) = 'input' and Attribute[lower-case(@Name) = 'type'][lower-case(normalize-space(string-join(AttributeValue/Text/@Image, ''))) = 'image'])
or (lower-case(@Name) = 'apex:commandbutton' and Attribute[lower-case(@Name) = 'image'])
or (lower-case(@Name) = 'area' and Attribute[lower-case(@Name) = 'href'])
][not(Attribute[lower-case(@Name) = ('alt', 'aria-label', 'aria-labelledby', 'html-aria-label', 'html-aria-labelledby')][AttributeValue/*[not(self::Text) or normalize-space(@Image) != '']])]
]]></value>
</property>
</properties>
</rule>
<!--
PMD reads .html, .htm, .xhtml, .xht, and .shtml files as HTML. Aura
markup (.cmp and .app) is read as HTML when the Code Analyzer settings
list those file types, or when PMD runs with force-language html on a
list of them. Tag and attribute names are compared without regard to
case or hyphens, so lightning-button-icon and lightning:buttonIcon,
and alternative-text and alternativeText, are the same name.
-->
<rule
name="ImageWithoutAltHtmlLwcAura"
language="html"
message="Add alternative text to this image: alt, or alternative-text on a Lightning component; leave it empty only if the image is decorative"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An img element, or a Lightning carousel image, has its alternative text attribute: alt on an img, alternative-text in a Lightning web component, or alternativeText in Aura. The text says what the image shows, or the attribute is empty when the image is decorative.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[
(lower-case(local-name()) = 'img' and not(@*[lower-case(local-name()) = 'alt']))
or (translate(lower-case(local-name()), '-:', '') = 'lightningcarouselimage'
and not(@*[translate(lower-case(local-name()), '-', '') = 'alternativetext']))
]
]]></value>
</property>
</properties>
</rule>
<rule
name="ImageWithEmptyAltHtmlLwcAura"
language="html"
message="Empty alternative text hides this image from screen readers: confirm that it is decorative, or describe it"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An img element, a Lightning carousel image, or a Lightning avatar whose alternative text is empty or blank is hidden from assistive technology, which is right only when it is decorative. A bound property counts as text.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[
(lower-case(local-name()) = 'img' and @*[lower-case(local-name()) = 'alt'][normalize-space(.) = ''])
or (translate(lower-case(local-name()), '-:', '') = ('lightningcarouselimage', 'lightningavatar')
and @*[translate(lower-case(local-name()), '-', '') = 'alternativetext'][normalize-space(.) = ''])
]
]]></value>
</property>
</properties>
</rule>
<rule
name="ButtonWithoutNameHtmlLwcAura"
language="html"
message="Give this button a name that a screen reader can announce: alt, alternative-text, aria-label, or aria-labelledby with text"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An image button, an image-map link, a Lightning button icon, or an icon-only Lightning button has a name that a screen reader can announce: alt on an input of type image or on an area with an href, alternative-text or alternativeText on a Lightning button icon, or an aria-label or aria-labelledby attribute, with text. A bound property counts as text.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[
(lower-case(local-name()) = 'input' and @*[lower-case(local-name()) = 'type'][lower-case(normalize-space(.)) = 'image'])
or (lower-case(local-name()) = 'area' and @*[lower-case(local-name()) = 'href'])
or translate(lower-case(local-name()), '-:', '') = ('lightningbuttonicon', 'lightningbuttoniconstateful')
or (translate(lower-case(local-name()), '-:', '') = 'lightningbutton'
and @*[translate(lower-case(local-name()), '-', '') = 'iconname']
and not(@*[lower-case(local-name()) = 'label'][normalize-space(.) != '']))
][not(@*[translate(lower-case(local-name()), '-', '') = ('alt', 'alternativetext', 'arialabel', 'arialabelledby')][normalize-space(.) != ''])]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: VISUALFORCE, LWC, AND AURA MARKUP -->
<!-- ============================================================ -->
<!--
Reads the controller and extensions attributes of the apex:page tag,
and reports on the page, at the attribute, and not on the class. The
extensions attribute is a list, and one name that does not match is
enough. A page with a standardController only is not reported. Each
property is a regular expression that the whole class name must match.
-->
<rule
name="VisualforceControllerNaming"
language="visualforce"
message="A class named here does not match the naming pattern (controller: ${controllerPattern}; extension: ${extensionPattern})"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The custom controller and the controller extensions of a Visualforce page are named for their role, so that a class bound to a page can be told by its name. The controllerPattern and extensionPattern properties hold the patterns the names must match, and each organisation sets its own.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'apex:page']/Attribute[lower-case(@Name) = 'controller'][not(matches(string-join(AttributeValue/Text/@Image, ''), concat('^(', $controllerPattern, ')$')))]
| //Element[lower-case(@Name) = 'apex:page']/Attribute[lower-case(@Name) = 'extensions'][some $e in tokenize(normalize-space(string-join(AttributeValue/Text/@Image, '')), '\s*,\s*') satisfies not(matches($e, concat('^(', $extensionPattern, ')$')))]
]]></value>
</property>
<property
name="controllerPattern"
type="String"
value=".*Controller"
description="A regular expression that the name of a custom controller must match" />
<property
name="extensionPattern"
type="String"
value=".*(Extension|Ext)"
description="A regular expression that the name of a controller extension must match" />
</properties>
</rule>
<!--
Two rules on a path written out in a Visualforce page or component,
where a global variable gives the same address. Each reads attribute
values, script blocks, and quoted strings inside an expression. The page
link rule wants a page name after /apex/, so the words /apex/ in a
sentence are not reported.
-->
<rule
name="AvoidHardcodedResourcePathVf"
language="visualforce"
message="Reference this static resource through $Resource, not a /resource/ path"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A static resource is referenced by name through the $Resource global variable, with URLFOR for a file inside an archive. A hard-coded /resource/ path is not adjusted for the path prefix of an Experience Cloud site.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Text[contains(@Image, '/resource/')] | //Literal[contains(@Image, '/resource/')]
]]></value>
</property>
</properties>
</rule>
<rule
name="AvoidHardcodedPageLinkVf"
language="visualforce"
message="Link to this page through $Page, not a hard-coded /apex/ path"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A link to another Visualforce page is written with the $Page global variable. A hard-coded /apex/ path is not adjusted for the path prefix of an Experience Cloud site.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Text[matches(@Image, '/apex/[A-Za-z]')] | //Literal[matches(@Image, '/apex/[A-Za-z]')]
]]></value>
</property>
</properties>
</rule>
<!--
The same standard as PMD's AvoidInlineStyles, which reads Lightning
templates and not Visualforce.
-->
<rule
name="AvoidInlineStylesInVisualforce"
language="visualforce"
message="Move this inline style to a stylesheet"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Styling belongs in a stylesheet, where it can be reused and reviewed. A style element, and a style, labelStyle, or html-style attribute on an HTML tag or on a Visualforce component, are reported; styleClass and class are not.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Attribute[lower-case(@Name) = ('style', 'labelstyle', 'html-style')] | //Element[lower-case(@Name) = 'style']
]]></value>
</property>
</properties>
</rule>
<!--
Two rules on a javascript: URL: one for Visualforce pages and
components, and one for Lightning Web Component templates, Aura markup,
and plain HTML files. Each reports href="javascript:..." and an event
attribute such as onclick that starts with javascript:, in any case, and
the Visualforce rule also a string inside an expression. ESLint's
no-script-url makes the same check in JavaScript files. The
allowedPattern property holds the values that may be written: by
default javascript:void(0), a link that does nothing, which generated
documentation and older markup often hold.
-->
<rule
name="AvoidJavaScriptUrlVf"
language="visualforce"
message="Do not use a javascript: URL - handle the event in a script"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An attribute value that starts with javascript: runs its text as code. Handle the event in a script, and give a link a real address. The allowedPattern property holds the values that may be written, by default javascript:void(0), and each organisation sets its own.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Attribute/AttributeValue/Text[matches(@Image, '^\s*javascript:', 'i')][not(matches(@Image, $allowedPattern, 'i'))]
| //Attribute/AttributeValue/ElExpression//Literal[matches(@Image, '^.\s*javascript:', 'i')][not(matches(substring(@Image, 2, string-length(@Image) - 2), $allowedPattern, 'i'))]
]]></value>
</property>
<property
name="allowedPattern"
type="String"
value="^\s*javascript:\s*void\s*\(\s*0\s*\)\s*;?\s*$"
description="A regular expression for the javascript: values that may be written; case is ignored" />
</properties>
</rule>
<rule
name="AvoidJavaScriptUrlHtmlLwcAura"
language="html"
message="Do not use a javascript: URL - handle the event in a script"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An attribute value that starts with javascript: runs its text as code. Handle the event in the component's JavaScript, and give a link a real address. This rule reads Lightning Web Component templates, Aura markup, and plain HTML files. The allowedPattern property holds the values that may be written, by default javascript:void(0), and each organisation sets its own.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[@*[let $v := replace(string(.), "^\s*\{!\s*'(.*)'\s*\}\s*$", '$1') return matches($v, '^\s*javascript:', 'i') and not(matches($v, $allowedPattern, 'i'))]]
]]></value>
</property>
<property
name="allowedPattern"
type="String"
value="^\s*javascript:\s*void\s*\(\s*0\s*\)\s*;?\s*$"
description="A regular expression for the javascript: values that may be written; case is ignored" />
</properties>
</rule>
<!--
The markup form of AvoidAbsoluteSalesforceUrl, the Apex rule, and of
AvoidAbsoluteSalesforceUrlFlow, with the same host pattern and the same
allowedSubdomains property. Each splits text at white space and
quotation marks and checks each URL by itself, as the Flow rule does, so
a help link in the same text does not excuse an org URL. The Visualforce
rule reads attribute values, page text, script text, and strings inside
an expression; a host that a merge field supplies, as in
{!$Site.Domain}/s/, is not written out. The Html rule reads attribute
values and text.
-->
<rule
name="AvoidAbsoluteSalesforceUrlVf"
language="visualforce"
message="Do not hard-code a Salesforce URL - use a relative URL, $Site, URLFOR, or $Page"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The absolute URL of a Salesforce org is right in one org only, because every sandbox and every production org has a host name of its own. A link is relative, or built from $Site, URLFOR, or $Page. The allowedSubdomains property holds the subdomains of salesforce.com that may be written out, and each organisation adds its own.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(Text | ElExpression//Literal)[some $t in tokenize(@Image, '[\s"''<>]+') satisfies (matches($t, '^https?://([a-z0-9-]+\.)+((salesforce|force|visualforce|salesforce-sites|salesforce-setup|cloudforce)\.com|my\.site\.com)', 'i') and not(some $s in $allowedSubdomains satisfies matches($t, concat('^https?://', $s, '\.salesforce\.com'), 'i')))]
]]></value>
</property>
<property
name="allowedSubdomains"
type="List[String]"
value="www,help,developer,trailhead,status,trust,appexchange"
description="The subdomains of salesforce.com whose URLs may be written out, each as the part of the host before .salesforce.com" />
</properties>
</rule>
<rule
name="AvoidAbsoluteSalesforceUrlHtml"
language="html"
message="Do not hard-code a Salesforce URL - use a relative URL or a bound property"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The absolute URL of a Salesforce org is right in one org only, because every sandbox and every production org has a host name of its own. A link is relative, or comes from a bound property that the component's JavaScript builds. This rule reads Lightning Web Component templates and Aura markup. The allowedSubdomains property holds the subdomains of salesforce.com that may be written out, and each organisation adds its own.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[@*[some $t in tokenize(string(.), '[\s"''<>]+') satisfies (matches($t, '^https?://([a-z0-9-]+\.)+((salesforce|force|visualforce|salesforce-sites|salesforce-setup|cloudforce)\.com|my\.site\.com)', 'i') and not(some $s in $allowedSubdomains satisfies matches($t, concat('^https?://', $s, '\.salesforce\.com'), 'i')))]]
| //text()[some $t in tokenize(string(.), '[\s"''<>]+') satisfies (matches($t, '^https?://([a-z0-9-]+\.)+((salesforce|force|visualforce|salesforce-sites|salesforce-setup|cloudforce)\.com|my\.site\.com)', 'i') and not(some $s in $allowedSubdomains satisfies matches($t, concat('^https?://', $s, '\.salesforce\.com'), 'i')))]
]]></value>
</property>
<property
name="allowedSubdomains"
type="List[String]"
value="www,help,developer,trailhead,status,trust,appexchange"
description="The subdomains of salesforce.com whose URLs may be written out, each as the part of the host before .salesforce.com" />
</properties>
</rule>
<!--
Three rules on the structure of a Visualforce page. A link to a
hard-coded record Id reads link attributes whose text starts with / and
an Id of 15 or 18 characters, with the Id pattern of
AvoidHardcodedIdFlow; a link built from an expression, such as
/{!acc.Id}, is not reported. The form rule reports every apex:form after
the first in the file. The frame rule reports apex:iframe and iframe.
-->
<rule
name="AvoidHardcodedIdVf"
language="visualforce"
message="Do not link to a hard-coded record Id - use URLFOR with $Action, or an expression"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A record Id belongs to one org, so a link to one is wrong in every sandbox and every other org. Build the link with URLFOR and $Action, or from a field of the record.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Attribute[lower-case(@Name) = ('href', 'value', 'src', 'action', 'url')]/AttributeValue/Text[matches(@Image, '^\s*/[a-zA-Z0-9]{5}0[a-zA-Z0-9]{9}([a-zA-Z0-9]{3})?([^A-Za-z0-9_]|$)')]
]]></value>
</property>
</properties>
</rule>
<rule
name="AvoidMultipleFormsVf"
language="visualforce"
message="Use one apex:form on this page, with apex:actionRegion for a part that posts on its own"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Salesforce calls one apex:form in a page or custom component a best practice. A part of the page that posts on its own uses an apex:actionRegion inside the one form.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
(//Element[lower-case(@Name) = 'apex:form'])[position() > 1]
]]></value>
</property>
</properties>
</rule>
<rule
name="AvoidIframeVf"
language="visualforce"
message="Do not frame a page - include a component, or link to the page"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Visualforce pages and custom iframes are not supported in Lightning Experience on iPad Safari. A page includes a component, or links to the page. A page that embeds a third-party site has no other way, and its owning team accepts the report.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = ('apex:iframe', 'iframe')]
]]></value>
</property>
</properties>
</rule>
<!--
Reads every attribute whose name starts with on, on an element under an
aura:component or aura:application root, and reports the element when
the value is not a {!...} expression. A value that starts with
javascript: is left to AvoidJavaScriptUrlHtmlLwcAura, so an attribute
gets at most one report, and javascript:void(0) none. A Lightning Web
Component template has a template root, and its handlers are {handler}
expressions that the compiler checks.
-->
<rule
name="NoInlineEventHandlerAura"
language="html"
message="Name a controller action in this event attribute, not a script"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An event attribute in Aura markup names a controller action in an expression. The Content Security Policy of the platform blocks an inline script, so a handler written as a script does nothing.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = ('aura:component', 'aura:application')]//*[@*[starts-with(lower-case(name()), 'on')][not(matches(normalize-space(string(.)), '^(\{!|javascript:)', 'i'))]]
]]></value>
</property>
</properties>
</rule>
<!--
Accessibility checks on tables, form groups, frames, lists, landmarks,
media, and the keyboard. Each is written twice, because PMD reads the
two kinds of markup with two parsers: the rule that ends in Vf reads
Visualforce pages and components, and the rule that ends in Html reads
Lightning Web
Component templates and Aura markup. A tag name is compared without
regard to case. A table whose role is presentation or none is a layout
table, and the data table rules leave it alone. A caption, th, header
attribute, or legend counts only for the nearest table or fieldset
that holds it. In a list, a tag that only repeats or groups its
content (a tag with a colon, such as apex:repeat or aura:iteration, or
a template tag) may sit between the list and its items. An apex:page
or an aura:application counts as the parent of the items in it.
-->
<rule
name="TableWithoutCaptionVf"
language="visualforce"
message="Give this table a caption or an aria-label"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A data table says what it holds, in a caption element, a caption facet, or an aria-label, aria-labelledby, or aria-describedby attribute with text. This applies to a table element, an apex:dataTable, and an apex:pageBlockTable. A caption of a table inside this table does not count. A table with a presentation role is a layout table, not a data table. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = ('table', 'apex:datatable', 'apex:pageblocktable')][not(some $c in .//Element[lower-case(@Name) = 'caption' or (lower-case(@Name) = 'apex:facet' and Attribute[lower-case(@Name) = 'name']/AttributeValue/Text[@Image = 'caption'])] satisfies $c/ancestor::Element[lower-case(@Name) = ('table', 'apex:datatable', 'apex:pageblocktable')][1] is .)][not(Attribute[lower-case(@Name) = ('aria-label', 'aria-labelledby', 'aria-describedby', 'html-aria-label', 'html-aria-labelledby', 'html-aria-describedby')][AttributeValue/*[not(self::Text) or normalize-space(@Image) != '']])][not(Attribute[lower-case(@Name) = 'role']/AttributeValue/Text[lower-case(normalize-space(@Image)) = ('presentation', 'none')])]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableWithoutCaptionHtml"
language="html"
message="Give this table a caption or an aria-label"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A data table says what it holds, in a caption element, or in an aria-label, aria-labelledby, or aria-describedby attribute with text. A caption of a table inside this table does not count. A table with a presentation role is a layout table, not a data table. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'table'][not(some $c in .//*[lower-case(local-name()) = 'caption'] satisfies $c/ancestor::*[lower-case(local-name()) = 'table'][1] is .)][not((@aria-label, @aria-labelledby, @aria-describedby)[normalize-space(.) != ''])][not(lower-case(normalize-space(@role)) = ('presentation', 'none'))]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableWithoutHeadersVf"
language="visualforce"
message="Mark the header cells of this table with th"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A data table marks its header cells with th, so that assistive technology can announce the headers of each cell. A th of a table inside this table does not count. A table with a presentation role is a layout table, not a data table. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'table'][not(some $h in .//Element[lower-case(@Name) = 'th'] satisfies $h/ancestor::Element[lower-case(@Name) = 'table'][1] is .)][not(Attribute[lower-case(@Name) = 'role']/AttributeValue/Text[lower-case(normalize-space(@Image)) = ('presentation', 'none')])]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableWithoutHeadersHtml"
language="html"
message="Mark the header cells of this table with th"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A data table marks its header cells with th, so that assistive technology can announce the headers of each cell. A th of a table inside this table does not count. A table with a presentation role is a layout table, not a data table. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'table'][not(some $h in .//*[lower-case(local-name()) = 'th'] satisfies $h/ancestor::*[lower-case(local-name()) = 'table'][1] is .)][not(lower-case(normalize-space(@role)) = ('presentation', 'none'))]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableHeaderWithoutScopeVf"
language="visualforce"
message="Give this header cell a scope or an id"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A header cell says which cells it heads: a scope attribute for a row or a column, or an id that cells name in their headers attribute. A th in a layout table is not reported here, because the layout table rules report it. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'th'][not(Attribute[lower-case(@Name) = 'scope'][AttributeValue/*[not(self::Text) or normalize-space(@Image) != '']])][not(Attribute[lower-case(@Name) = 'id']/AttributeValue/ElExpression)][not(ancestor::Element[lower-case(@Name) = 'table'][1]//Element[lower-case(@Name) = ('td', 'th')]/Attribute[lower-case(@Name) = 'headers']/AttributeValue/ElExpression)][not(Attribute[lower-case(@Name) = 'id']/AttributeValue/Text/@Image = ancestor::Element[lower-case(@Name) = 'table'][1]//Element[lower-case(@Name) = ('td', 'th')]/Attribute[lower-case(@Name) = 'headers']/AttributeValue/Text/tokenize(normalize-space(@Image), '\s+'))][not(ancestor::Element[lower-case(@Name) = 'table'][1][Attribute[lower-case(@Name) = 'role']/AttributeValue/Text[lower-case(normalize-space(@Image)) = ('presentation', 'none')]])]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableHeaderWithoutScopeHtml"
language="html"
message="Give this header cell a scope or an id"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A header cell says which cells it heads: a scope attribute for a row or a column, or an id that cells name in their headers attribute. A th in a layout table is not reported here, because the layout table rules report it. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'th'][not(normalize-space(@scope) != '')][not(contains(@id, '{'))][not(ancestor::*[lower-case(local-name()) = 'table'][1]//*[lower-case(local-name()) = ('td', 'th')][contains(@headers, '{')])][not(@id = ancestor::*[lower-case(local-name()) = 'table'][1]//*[lower-case(local-name()) = ('td', 'th')]/tokenize(normalize-space(@headers), '\s+'))][not(ancestor::*[lower-case(local-name()) = 'table'][1][lower-case(normalize-space(@role)) = ('presentation', 'none')])]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableCellHeaderNotFoundVf"
language="visualforce"
message="This headers attribute names an id that no header cell of the table has"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every id named in a headers attribute is the id of a th in the same table. A name that matches nothing leaves the cell with no header. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = ('td', 'th')]/Attribute[lower-case(@Name) = 'headers'][not(AttributeValue/ElExpression)][some $h in tokenize(normalize-space(string-join(AttributeValue/Text/@Image, ' ')), '\s+') satisfies not($h = ancestor::Element[lower-case(@Name) = 'table'][1]//Element[lower-case(@Name) = 'th']/Attribute[lower-case(@Name) = 'id']/AttributeValue/Text/@Image)]
]]></value>
</property>
</properties>
</rule>
<rule
name="TableCellHeaderNotFoundHtml"
language="html"
message="This headers attribute names an id that no header cell of the table has"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every id named in a headers attribute is the id of a th in the same table. A name that matches nothing leaves the cell with no header. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = ('td', 'th')][@headers][not(contains(@headers, '{'))][some $h in tokenize(normalize-space(@headers), '\s+') satisfies not($h = ancestor::*[lower-case(local-name()) = 'table'][1]//*[lower-case(local-name()) = 'th']/@id)]
]]></value>
</property>
</properties>
</rule>
<rule
name="LayoutTableVf"
language="visualforce"
message="Do not use a table for layout - use CSS"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A table with the role presentation or none is a table used for layout. Lay the content out in CSS, and keep tables for data. This rule reads Visualforce pages and components.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'table'][Attribute[lower-case(@Name) = 'role']/AttributeValue/Text[lower-case(normalize-space(@Image)) = ('presentation', 'none')]]
]]></value>
</property>
</properties>
</rule>
<rule
name="LayoutTableHtml"
language="html"
message="Do not use a table for layout - use CSS"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A table with the role presentation or none is a table used for layout. Lay the content out in CSS, and keep tables for data. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'table'][lower-case(normalize-space(@role)) = ('presentation', 'none')]
]]></value>
</property>
</properties>
</rule>
<rule
name="LayoutTableWithSemanticMarkupVf"
language="visualforce"
message="Remove the caption, header cells, and header attributes from this layout table"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A layout table carries no caption, th, summary, headers, or scope. That markup tells assistive technology that the table holds data, and a screen reader then reads the layout as a data table. The markup of a data table inside the layout table does not count. This rule reads Visualforce pages and components.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'table'][Attribute[lower-case(@Name) = 'role']/AttributeValue/Text[lower-case(normalize-space(@Image)) = ('presentation', 'none')]][Attribute[lower-case(@Name) = 'summary'] or (some $m in .//Element[lower-case(@Name) = ('th', 'caption') or Attribute[lower-case(@Name) = ('headers', 'scope')]] satisfies $m/ancestor::Element[lower-case(@Name) = 'table'][1] is .)]
]]></value>
</property>
</properties>
</rule>
<rule
name="LayoutTableWithSemanticMarkupHtml"
language="html"
message="Remove the caption, header cells, and header attributes from this layout table"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A layout table carries no caption, th, summary, headers, or scope. That markup tells assistive technology that the table holds data, and a screen reader then reads the layout as a data table. The markup of a data table inside the layout table does not count. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'table'][lower-case(normalize-space(@role)) = ('presentation', 'none')][@summary or (some $m in .//*[lower-case(local-name()) = ('th', 'caption') or @headers or @scope] satisfies $m/ancestor::*[lower-case(local-name()) = 'table'][1] is .)]
]]></value>
</property>
</properties>
</rule>
<rule
name="FieldsetWithoutLegendVf"
language="visualforce"
message="Give this fieldset a legend"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A fieldset has a legend that names the group of controls inside it. A legend of a fieldset inside this one does not count. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'fieldset'][not(some $l in .//Element[lower-case(@Name) = 'legend'] satisfies $l/ancestor::Element[lower-case(@Name) = 'fieldset'][1] is .)]
]]></value>
</property>
</properties>
</rule>
<rule
name="FieldsetWithoutLegendHtml"
language="html"
message="Give this fieldset a legend"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A fieldset has a legend that names the group of controls inside it. A legend of a fieldset inside this one does not count. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'fieldset'][not(some $l in .//*[lower-case(local-name()) = 'legend'] satisfies $l/ancestor::*[lower-case(local-name()) = 'fieldset'][1] is .)]
]]></value>
</property>
</properties>
</rule>
<rule
name="FrameWithoutTitleVf"
language="visualforce"
message="Give this frame a title"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A frame has a title attribute that says what the frame holds, so that a screen reader user can decide whether to enter it. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = ('iframe', 'frame', 'apex:iframe')][not(Attribute[lower-case(@Name) = 'title'][AttributeValue/*[not(self::Text) or normalize-space(@Image) != '']])]
]]></value>
</property>
</properties>
</rule>
<rule
name="FrameWithoutTitleHtml"
language="html"
message="Give this frame a title"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A frame has a title attribute that says what the frame holds, so that a screen reader user can decide whether to enter it. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = ('iframe', 'frame')][not(normalize-space(@title) != '')]
]]></value>
</property>
</properties>
</rule>
<rule
name="ListItemOutsideListVf"
language="visualforce"
message="Put this list item inside a list element"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An li belongs in a ul, ol, or menu, and a dt or dd in a dl. Outside a list, a screen reader does not announce the items as a list. A div of dt and dd elements may sit in a dl. An apex: tag may sit between the list and its items. In a page, an li with only apex: tags above it is reported. In a component, it is not reported, because its list can be in the page that uses it. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'li'][ancestor::Element[not(contains(@Name, ':')) or lower-case(@Name) = 'apex:page'][1][not(lower-case(@Name) = ('ul', 'ol', 'menu'))]] | //Element[lower-case(@Name) = ('dt', 'dd')][ancestor::Element[not(contains(@Name, ':')) or lower-case(@Name) = 'apex:page'][1][not(lower-case(@Name) = 'dl' or (lower-case(@Name) = 'div' and ancestor::Element[not(contains(@Name, ':')) or lower-case(@Name) = 'apex:page'][1][lower-case(@Name) = 'dl']))]]
]]></value>
</property>
</properties>
</rule>
<rule
name="ListItemOutsideListHtml"
language="html"
message="Put this list item inside a list element"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An li belongs in a ul, ol, or menu, and a dt or dd in a dl. Outside a list, a screen reader does not announce the items as a list. A div of dt and dd elements may sit in a dl. A template tag, or a tag with a colon such as aura:iteration, may sit between the list and its items. An li at the root of a component is not reported, because its list can be in the parent. An li with only such tags above it in an aura:application is reported. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'li'][ancestor::*[parent::*][not(lower-case(local-name()) = 'template')][not(contains(local-name(), ':')) or lower-case(local-name()) = 'aura:application'][1][not(lower-case(local-name()) = ('ul', 'ol', 'menu'))]] | //*[lower-case(local-name()) = ('dt', 'dd')][ancestor::*[parent::*][not(lower-case(local-name()) = 'template')][not(contains(local-name(), ':')) or lower-case(local-name()) = 'aura:application'][1][not(lower-case(local-name()) = 'dl' or (lower-case(local-name()) = 'div' and ancestor::*[parent::*][not(lower-case(local-name()) = 'template')][not(contains(local-name(), ':')) or lower-case(local-name()) = 'aura:application'][1][lower-case(local-name()) = 'dl']))]]
]]></value>
</property>
</properties>
</rule>
<rule
name="RepeatedLandmarkWithoutUniqueLabelVf"
language="visualforce"
message="Give each landmark with this role a unique aria-label or aria-labelledby"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>When a file has two or more landmarks with the same role, each has an aria-label or aria-labelledby attribute with text, and no two of them have the same text, so that a screen reader user can tell them apart. A landmark is a nav, aside, search, or main element, a form element with an aria-label or aria-labelledby, or an element whose role attribute names a landmark role: banner, complementary, contentinfo, form, main, navigation, region, or search. A form with no name is not a landmark. A merge expression counts as a unique label, because its value is not known. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
let $roles := ('banner', 'complementary', 'contentinfo', 'form', 'main', 'navigation', 'region', 'search'),
$labelOf := function($e) {
let $a := ($e/Attribute[lower-case(@Name) = 'aria-labelledby'], $e/Attribute[lower-case(@Name) = 'aria-label'])[AttributeValue/*[not(self::Text) or normalize-space(@Image) != '']][1]
return if (empty($a)) then ''
else if ($a/AttributeValue/*[not(self::Text)]) then 'expression'
else concat(lower-case($a/@Name), ':', lower-case(normalize-space(string-join($a/AttributeValue/Text/@Image, ''))))
},
$roleOf := function($e) {
let $r := tokenize(lower-case(normalize-space(string-join($e/Attribute[lower-case(@Name) = 'role']/AttributeValue/Text/@Image, ''))), ' ')[1],
$n := lower-case($e/@Name)
return if ($r) then $r
else if ($n = 'nav') then 'navigation'
else if ($n = 'aside') then 'complementary'
else if ($n = ('search', 'main')) then $n
else if ($n = 'form' and $labelOf($e) != '') then 'form'
else ''
},
$landmarks := //Element[$roleOf(.) = $roles]
return $landmarks[
let $r := $roleOf(.), $l := $labelOf(.), $same := $landmarks[$roleOf(.) = $r]
return count($same) > 1 and ($l = '' or ($l != 'expression' and count($same[$labelOf(.) = $l]) > 1))
]
]]></value>
</property>
</properties>
</rule>
<rule
name="RepeatedLandmarkWithoutUniqueLabelHtml"
language="html"
message="Give each landmark with this role a unique aria-label or aria-labelledby"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>When a file has two or more landmarks with the same role, each has an aria-label or aria-labelledby attribute with text, and no two of them have the same text, so that a screen reader user can tell them apart. A landmark is a nav, aside, search, or main element, a form element with an aria-label or aria-labelledby, or an element whose role attribute names a landmark role: banner, complementary, contentinfo, form, main, navigation, region, or search. A form with no name is not a landmark. A bound property or an expression counts as a unique label, because its value is not known. Landmarks in two components that show on one page are not compared. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
let $roles := ('banner', 'complementary', 'contentinfo', 'form', 'main', 'navigation', 'region', 'search'),
$labelOf := function($e) {
let $a := ($e/@*[lower-case(local-name()) = 'aria-labelledby'], $e/@*[lower-case(local-name()) = 'aria-label'])[normalize-space(.) != ''][1]
return if (empty($a)) then ''
else if (starts-with(normalize-space($a), '{')) then 'expression'
else concat(lower-case(local-name($a)), ':', lower-case(normalize-space($a)))
},
$roleOf := function($e) {
let $r := tokenize(lower-case(normalize-space(string(($e/@*[lower-case(local-name()) = 'role'])[1]))), ' ')[1],
$n := lower-case(local-name($e))
return if ($r) then $r
else if ($n = 'nav') then 'navigation'
else if ($n = 'aside') then 'complementary'
else if ($n = ('search', 'main')) then $n
else if ($n = 'form' and $labelOf($e) != '') then 'form'
else ''
},
$landmarks := //*[$roleOf(.) = $roles]
return $landmarks[
let $r := $roleOf(.), $l := $labelOf(.), $same := $landmarks[$roleOf(.) = $r]
return count($same) > 1 and ($l = '' or ($l != 'expression' and count($same[$labelOf(.) = $l]) > 1))
]
]]></value>
</property>
</properties>
</rule>
<rule
name="ObjectWithoutAlternativeVf"
language="visualforce"
message="Give this object element fallback content"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An object element holds fallback content, text or markup, for a browser or a reader that cannot show the object. An object that holds only param elements is reported. This rule reads Visualforce pages and components.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'object'][not(Content/*[not(self::Element[lower-case(@Name) = 'param'])][not(self::Text[normalize-space(@Image) = ''])])]
]]></value>
</property>
</properties>
</rule>
<rule
name="ObjectWithoutAlternativeHtml"
language="html"
message="Give this object element fallback content"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An object element holds fallback content, text or markup, for a browser or a reader that cannot show the object. An object that holds only param elements is reported. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'object'][not(*[not(lower-case(local-name()) = 'param')])][normalize-space(string-join(text(), '')) = '']
]]></value>
</property>
</properties>
</rule>
<rule
name="VideoWithoutCaptionsVf"
language="visualforce"
message="Add a captions or subtitles track to this video"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A video element has a track element whose kind is captions or subtitles, or that has no kind, which a browser treats as subtitles, so that a viewer who cannot hear the audio can follow it. This rule reads Visualforce pages and components.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[lower-case(@Name) = 'video'][not(.//Element[lower-case(@Name) = 'track'][not(Attribute[lower-case(@Name) = 'kind']) or Attribute[lower-case(@Name) = 'kind']/AttributeValue/Text[lower-case(normalize-space(@Image)) = ('captions', 'subtitles')]])]
]]></value>
</property>
</properties>
</rule>
<rule
name="VideoWithoutCaptionsHtml"
language="html"
message="Add a captions or subtitles track to this video"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A video element has a track element whose kind is captions or subtitles, or that has no kind, which a browser treats as subtitles, so that a viewer who cannot hear the audio can follow it. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[lower-case(local-name()) = 'video'][not(.//*[lower-case(local-name()) = 'track'][not(@kind) or lower-case(normalize-space(@kind)) = ('captions', 'subtitles')])]
]]></value>
</property>
</properties>
</rule>
<rule
name="ServerSideImageMapVf"
language="visualforce"
message="Do not use a server-side image map - use a client-side map or separate links"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The ismap attribute makes an image a server-side image map, which sends the coordinates of a click and cannot be worked from the keyboard. Use a client-side map, or separate links. This rule reads Visualforce pages and components.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element/Attribute[lower-case(@Name) = 'ismap'][not(lower-case(../@Name) = 'apex:image' and AttributeValue/Text[lower-case(normalize-space(@Image)) = 'false'])]
]]></value>
</property>
</properties>
</rule>
<rule
name="ServerSideImageMapHtml"
language="html"
message="Do not use a server-side image map - use a client-side map or separate links"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The ismap attribute makes an image a server-side image map, which sends the coordinates of a click and cannot be worked from the keyboard. Use a client-side map, or separate links. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[@ismap]
]]></value>
</property>
</properties>
</rule>
<rule
name="MouseEventWithoutKeyboardEventVf"
language="visualforce"
message="Give this mouse handler a keyboard equivalent: onfocus for onmouseover, onblur for onmouseout, or a key handler for onclick"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every function that a mouse reaches, a keyboard reaches too. An onmouseover has an onfocus, an onmouseout has an onblur, and an onclick has an onkeydown, onkeyup, or onkeypress. An onclick is not reported on an element that already answers the keyboard: a link, an image-map area, a button, a form control, a summary, or a Visualforce component that renders one of them. An apex:outputPanel renders a div or a span, so its onclick is reported. This rule reads Visualforce pages and components.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Element[Attribute[lower-case(@Name) = 'onmouseover']][not(Attribute[lower-case(@Name) = 'onfocus'])]
| //Element[Attribute[lower-case(@Name) = 'onmouseout']][not(Attribute[lower-case(@Name) = 'onblur'])]
| //Element[Attribute[lower-case(@Name) = 'onclick']][not(Attribute[lower-case(@Name) = ('onkeydown', 'onkeyup', 'onkeypress')])][not(lower-case(@Name) = ('a', 'area', 'button', 'input', 'select', 'textarea', 'summary', 'apex:commandbutton', 'apex:commandlink', 'apex:inputcheckbox', 'apex:inputfield', 'apex:inputfile', 'apex:inputsecret', 'apex:inputtext', 'apex:inputtextarea', 'apex:outputlink', 'apex:selectcheckboxes', 'apex:selectlist', 'apex:selectradio'))]
]]></value>
</property>
</properties>
</rule>
<rule
name="MouseEventWithoutKeyboardEventHtml"
language="html"
message="Give this mouse handler a keyboard equivalent: onfocus for onmouseover, onblur for onmouseout, or a key handler for onclick"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Every function that a mouse reaches, a keyboard reaches too. An onmouseover has an onfocus, an onmouseout has an onblur, and an onclick has an onkeydown, onkeyup, or onkeypress. An onclick is not reported on an element that already answers the keyboard: a link, an image-map area, a button, a form control, or a summary. An onclick on a component, whose name has a hyphen or a colon, is not reported, because the component handles its own keyboard behaviour. This rule reads Lightning Web Component templates and Aura markup.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[@*[lower-case(local-name()) = 'onmouseover']][not(@*[lower-case(local-name()) = 'onfocus'])]
| //*[@*[lower-case(local-name()) = 'onmouseout']][not(@*[lower-case(local-name()) = 'onblur'])]
| //*[@*[lower-case(local-name()) = 'onclick']][not(@*[lower-case(local-name()) = ('onkeydown', 'onkeyup', 'onkeypress')])][not(lower-case(local-name()) = ('a', 'area', 'button', 'input', 'select', 'textarea', 'summary') or contains(local-name(), '-') or contains(local-name(), ':'))]
]]></value>
</property>
</properties>
</rule>
<!--
A script block in a Lightning Web Component template is not allowed by
the framework, so this check has no rule for Lightning markup.
-->
<rule
name="NestedScriptElement"
language="visualforce"
message="This script block holds a script tag - escape it or load the script another way"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>The HTML parser gives the text of a script tag a special meaning inside a script block, so the block can end somewhere other than where its author meant. Escape the tag, or load the other script with apex:includeScript.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//HtmlScript/Text[contains(lower-case(@Image), '<script')]
]]></value>
</property>
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: LIGHTNING OUT -->
<!-- ============================================================ -->
<!--
Lightning Out (beta) requires Lightning Locker, so it is reported.
An Aura application that extends ltng:outApp or ltng:outAppUnstyled is
a Lightning Out dependency app, and the rule reports the extends
attribute. Code Analyzer reads an Aura application (.app) as HTML when
the extension is listed under engines.pmd.file_extensions.html.
-->
<rule
name="NoLightningOutApplicationAura"
language="html"
message="Lightning Out (beta) requires Lightning Locker - use Lightning Out 2.0 for an external app, or record the reason for this use"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>An Aura application should not be a Lightning Out dependency app, which extends ltng:outApp or ltng:outAppUnstyled. Such an app is Lightning Out (beta), which requires Lightning Locker, and Lightning Web Security does not support it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[@extends = 'ltng:outApp' or @extends = 'ltng:outAppUnstyled']
]]></value>
</property>
</properties>
</rule>
<!--
A Visualforce page uses Lightning Out (beta) through apex:includeLightning,
or by loading lightning.out.js and calling $Lightning.use. The rule reports
each of them, in the tag, a script tag, an apex:includeScript, or a script
block.
-->
<rule
name="NoLightningOutInVisualforce"
language="visualforce"
message="Lightning Components for Visualforce is built on Lightning Out (beta), which requires Lightning Locker - remove it, or record the reason for this use"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A Visualforce page or component should not use Lightning Components for Visualforce, which is built on Lightning Out (beta): no apex:includeLightning, no lightning.out.js, and no call to $Lightning.use. Lightning Out (beta) requires Lightning Locker, and Lightning Web Security does not support it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Text[contains(@Image, 'lightning.out.js') or contains(@Image, '$Lightning.use')] | //Literal[contains(@Image, 'lightning.out.js') or contains(@Image, '$Lightning.use')] | //Element[lower-case(@Name) = 'apex:includelightning']
]]></value>
</property>
</properties>
</rule>
</ruleset>
The custom rules for Flows, Process Builder processes, and Workflow Rules.
Flow
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="DevSecOps Reference Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
Custom PMD rules for Flows, Process Builder processes, and Workflow Rules,
written in XPath. The other custom rules are in pmd-custom-rules-apex.xml,
pmd-custom-rules-markup.xml, pmd-custom-rules-metadata.xml, and, for
JavaScript, pmd-custom-rules-javascript.xml. The stock PMD rules this
reference adopts are in pmd-stock-rules.xml and, for JavaScript, in
pmd-stock-rules-javascript.xml. Adjust priorities and suppressions to
match your organisation's risk posture and coding standards.
</description>
<!--
Rule messages contain no apostrophes. PMD formats a message with Java's
MessageFormat, which treats a single apostrophe as a quoting character
and drops it, so "class's" would be printed as "classs".
-->
<exclude-pattern>.*/.sfdx/.*</exclude-pattern>
<exclude-pattern>.*/.sf/.*</exclude-pattern>
<exclude-pattern>.*/node_modules/.*</exclude-pattern>
<!-- ============================================================ -->
<!-- CUSTOM RULES: FLOW, PROCESS BUILDER, AND WORKFLOW RULES -->
<!-- ============================================================ -->
<rule
name="DoNotUseProcessBuilder"
language="xml"
message="Process Builder is deprecated - migrate this automation to Flow"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>All Process Builder automations must be migrated to Flow, except those from a managed package.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow[not(contains(pmd:fileName(), '__'))]/processType[. = ('Workflow', 'InvocableProcess', 'CustomEvent')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="DoNotRunFlowInSystemMode"
language="xml"
message="Do not run Flows in system mode; user mode is the default and is preferred"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Flags a Flow whose runInMode is SystemModeWithSharing or SystemModeWithoutSharing. Both bypass field-level security, and the latter bypasses sharing as well; a Flow in system mode needs a documented justification, as without sharing does in Apex.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/runInMode[starts-with(., 'SystemMode')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="ScheduledFlowMinimumApiVersion"
language="xml"
message="Scheduled Flows must run on API version 61.0 or later"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Scheduled-triggered Flows at API version 61.0 and later have higher asynchronous limits; older versions keep the old limits.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow[start/triggerType = 'Scheduled']/apiVersion[number(.) < 61]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="DoNotUseWorkflow"
language="xml"
message="Convert this Workflow Rule to a Flow"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Workflow Rules are retired; every Workflow Rule that is not from a managed package, active or inactive, must be rebuilt as a Flow.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Workflow/rules[not(contains(fullName, '__'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
A Get Records element is a recordLookups element, and each of its
conditions is a filters element. Reading every record of a custom
metadata type is ordinary, so an object whose name ends in __mdt is
not reported.
-->
<rule
name="FlowGetRecordsWithoutFilter"
language="xml"
message="Add a filter to this Get Records element"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Get Records element with no filter reads every record of the object, up to the limit on the records a transaction may retrieve. Add a condition that selects only the records the Flow needs.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/recordLookups[not(filters)][not(ends-with(object, '__mdt'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Reports the outer loop. A Flow is a graph: each element names the next
one in a targetReference. From the first element inside a loop (its
nextValueConnector), the rule follows every connector, and stops where
a path comes back to the loop. If one of the elements it passed is a
loop, that loop is inside this one. Two loops that follow one another
are not reported. The walk is a recursive function, which XPath 3.1
allows a rule to declare inline.
-->
<rule
name="AvoidNestedFlowLoops"
language="xml"
message="Do not put a loop inside a loop - this loop contains another"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A loop inside a loop runs its elements once for every pair of items, so the work grows with the product of the two collections. Filter or sort the inner collection before the loop, or move the work to invocable Apex that uses a map.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/loops[
let $flow := .., $self := string(name),
$walk := function($f, $todo as xs:string*, $seen as xs:string*) as xs:string* {
if (empty($todo)) then $seen
else if (head($todo) = ($seen, $self)) then $f($f, tail($todo), $seen)
else $f($f, (tail($todo), $flow/*[name = head($todo)]//targetReference/string()), ($seen, head($todo)))
}
return $flow/loops/name = $walk($walk, string(nextValueConnector/targetReference), ())
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Flow Builder records the layout in a processMetadataValues element
named CanvasMode. The rule reports at the first line of the file.
-->
<rule
name="FlowNotAutoLayout"
language="xml"
message="Switch this Flow to auto-layout"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Auto-layout places and aligns the elements of a Flow, so that every Flow reads the same way. A Flow in free-form layout is reported, and so is an older Flow that records no layout.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow[not(processType = ('Workflow', 'InvocableProcess', 'CustomEvent'))][not(processMetadataValues[name = 'CanvasMode']/value/stringValue = 'AUTO_LAYOUT_CANVAS')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Obsolete is the status that Setup shows as Inactive. Draft and
InvalidDraft are not reported here: FlowDraft reports them, for
information only.
-->
<rule
name="ObsoleteFlow"
language="xml"
message="Remove this inactive Flow, or activate it"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Flow whose status is Obsolete has been deactivated. Remove it from version control, or activate it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/status[. = 'Obsolete']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Draft is the status of a Flow that was never activated. Setup shows
it as Inactive, as it does an Obsolete Flow, and it shows InvalidDraft
as Draft. The rule informs the developer and blocks nothing, so a
Flow may still be deployed as a draft and activated by hand. Its name
and its check for Draft are from Thomas Prouvot's pmd-custom-rules
(MIT licence), https://github.com/tprouvot/pmd-custom-rules.
-->
<rule
name="FlowDraft"
language="xml"
message="Activate this Flow - it has never been active"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Flow whose status is Draft or InvalidDraft has never been activated, so it does not run for users. Activate it. The rule is for the developer's information.</description>
<priority>5</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/status[. = ('Draft', 'InvalidDraft')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Reads the text of every element of a Flow except a description, and
reports a 15-character or 18-character Salesforce ID, alone or inside
longer text such as a formula, a text template, or a URL. The pattern
needs the 0 that every ID has in its sixth place, and it does not check
the checksum of an 18-character ID. XPath has no \b, so the boundary on
each side is written as a character that is not a letter, a digit, or
an underscore. It replaces the Flow engine rule HardcodedId, which
never reports a 15-character ID.
Adapted from HardcodedId of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="AvoidHardcodedIdFlow"
language="xml"
message="Do not hard-code a record ID in a Flow - find the record by a field that is the same in every org"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A record ID belongs to one org, so a Flow that carries one breaks in every other org. Find the record by a field that is the same in every org, such as a DeveloperName or an external ID, or read it from a custom metadata type or a custom setting.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow//*[not(*)][not(local-name() = 'description')][matches(., '(^|[^A-Za-z0-9_])[a-zA-Z0-9]{5}0[a-zA-Z0-9]{9}([a-zA-Z0-9]{3})?([^A-Za-z0-9_]|$)')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
The Flow engine rule MissingFaultHandler reads Create, Update, and
Delete Records, actions, and subflows. This rule adds Get Records.
Adapted from MissingFaultPath of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="FlowGetRecordsWithoutFaultPath"
language="xml"
message="Add a fault path to this Get Records element"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Get Records element can fail, for example when the running user cannot read the object. Without a fault path, a failure ends the Flow with an unhandled fault and the user sees a generic error.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/recordLookups[not(faultConnector)]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
A record-triggered Flow is one whose start element has the trigger type
RecordBeforeSave, RecordAfterSave, or RecordBeforeDelete. The trigger
type is read, not the object, because a schedule-triggered Flow and a
platform event-triggered Flow also name an object. The rule reports
the trigger type.
-->
<rule
name="FlowMissingTriggerOrder"
language="xml"
message="Give this record-triggered Flow a trigger order"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Every record-triggered Flow has a trigger order, so that the Flows of one object run in a known order. A Flow with no trigger order runs in the order of its activation date.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow[not(triggerOrder)]/start/triggerType[. = ('RecordBeforeSave', 'RecordAfterSave', 'RecordBeforeDelete')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
The Flow form of AvoidAbsoluteSalesforceUrl, the Apex rule, with the
same host pattern and the same allowedSubdomains property. It reads the
text of every element of a Flow except a description, splits it at
white space and quotation marks, and checks each URL by itself, so a
help link in the same text does not excuse an org URL.
-->
<rule
name="AvoidAbsoluteSalesforceUrlFlow"
language="xml"
message="Do not hard-code a Salesforce URL in a Flow - use a relative URL"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The absolute URL of a Salesforce org is right in one org only, because every sandbox and every production org has a host name of its own. Use a relative URL. The allowedSubdomains property holds the subdomains of salesforce.com that may be written out, and each organisation adds its own.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow//*[not(*)][not(local-name() = 'description')][some $t in tokenize(., '[\s"''<>]+') satisfies (matches($t, '^https?://([a-z0-9-]+\.)+((salesforce|force|visualforce|salesforce-sites|salesforce-setup|cloudforce)\.com|my\.site\.com)', 'i') and not(some $s in $allowedSubdomains satisfies matches($t, concat('^https?://', $s, '\.salesforce\.com'), 'i')))]
]]></value>
</property>
<property
name="allowedSubdomains"
type="List[String]"
value="www,help,developer,trailhead,status,trust,appexchange"
description="The subdomains of salesforce.com whose URLs may be written out, each as the part of the host before .salesforce.com" />
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Reports an action or an Apex plug-in element in the body of a loop:
an element that the loop's next value connector leads to, from which a
path leads back to the loop. The walk is the one AvoidNestedFlowLoops
uses, and the second walk leaves out an element on a path that leaves
the loop, such as a fault path that ends the Flow, because that element
runs once.
Adapted from ActionCallsInLoop of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="AvoidFlowActionInLoop"
language="xml"
message="Do not call an action inside a loop - call it once, after the loop, with a collection"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>An action inside a loop runs once for each item, and each run can make its own queries and DML statements, or count against a limit of its own, such as the limit on emails. Build a collection in the loop, and call the action once, after the loop.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/(actionCalls | apexPluginCalls)[
let $flow := .., $me := string(name),
$walk := function($f, $stop as xs:string, $todo as xs:string*, $seen as xs:string*) as xs:string* {
if (empty($todo)) then $seen
else if (head($todo) = $seen) then $f($f, $stop, tail($todo), $seen)
else if (head($todo) = $stop) then $f($f, $stop, tail($todo), ($seen, head($todo)))
else $f($f, $stop, (tail($todo), $flow/*[name = head($todo)]//targetReference/string()), ($seen, head($todo)))
}
return some $loop in $flow/loops satisfies (
let $l := string($loop/name)
return $me = $walk($walk, $l, string($loop/nextValueConnector/targetReference), ())
and $l = $walk($walk, $l, $flow/*[name = $me]//targetReference/string(), ())
)
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Reads a Get Records element that returns one record. Its result is the
element itself when the fields are stored automatically, and otherwise
the record variable in its outputReference. The rule reports the
element when another element uses the result, by name or by one of its
fields (a connector that names the element as the next step, and the
declaration of the variable, are not uses), and no Decision condition
tests the result with IsNull or IsBlank. A Get Records that returns a collection is not
read, because a loop over an empty collection does nothing.
Adapted from MissingNullHandler of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="FlowGetRecordsWithoutNullCheck"
language="xml"
message="Check the result of this Get Records element for null before the Flow uses it"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Get Records element that finds no record returns null, and the next element that uses its result can fail. Add a Decision that checks the result for null before the Flow uses it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/recordLookups[getFirstRecordOnly = 'true'][
let $flow := .., $me := .,
$r := if (storeOutputAutomatically = 'true') then string(name) else string(outputReference)
return $r != ''
and (some $t in $flow/*[not(. is $me)][not(name = $r)]//*[not(*)][not(local-name() = 'targetReference')] satisfies
($t = $r or starts-with($t, concat($r, '.')) or contains($t, concat('{!', $r, '}')) or contains($t, concat('{!', $r, '.'))))
and not($flow/decisions/rules/conditions[leftValueReference = $r or starts-with(leftValueReference, concat($r, '.'))][operator = ('IsNull', 'IsBlank')])
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Reports a screen of a screen Flow that shows a Previous button (its
allowBack is true, and its footer is not hidden) and that a path from a
Create, Update, or Delete Records element reaches. A screen with no
Previous button ends the path, because the user cannot go back past
it. The element's own fault path is not followed: when the change
failed, running it again makes no second record.
Adapted from DuplicateDMLOperation of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="FlowAllowBackAfterRecordChange"
language="xml"
message="Do not let the user go back past this record change - hide the Previous button on this screen"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A user who goes back to a screen before a Create, Update, or Delete Records element, and then forward again, runs the element a second time, so a record can be created or changed twice. A screen after a record change does not offer the Previous button.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow[processType = 'Flow']/screens[allowBack = 'true'][not(showFooter = 'false')][
let $flow := .., $me := string(name),
$walk := function($f, $todo as xs:string*, $seen as xs:string*) as xs:string* {
if (empty($todo)) then $seen
else if (head($todo) = $seen) then $f($f, tail($todo), $seen)
else if ($flow/screens[name = head($todo)][not(allowBack = 'true') or showFooter = 'false']) then $f($f, tail($todo), ($seen, head($todo)))
else $f($f, (tail($todo), $flow/*[name = head($todo)]//targetReference/string()), ($seen, head($todo)))
}
return some $d in $flow/(recordCreates | recordUpdates | recordDeletes) satisfies
$me = $walk($walk, $d/*[not(self::faultConnector)]//targetReference/string(), ())
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
A Get Records element that stores all fields automatically, and names
none, reads every field of the object.
Adapted from GetRecordAllFields of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="FlowGetRecordsAllFields"
language="xml"
message="Name the fields that this Get Records element needs"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Get Records element that stores all fields automatically reads every field of the records it finds. Name only the fields that the Flow uses, to reduce the work and the data the Flow holds.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow/recordLookups[storeOutputAutomatically = 'true'][not(queriedFields)]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
A URL, a Flow quick action, and a Flow component on a record page each
give an input record variable named recordId the whole record, though
they pass only its ID. Apex passes a record
only if it has one, so an autolaunched Flow is not read.
Adapted from RecordIdAsString of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="ScreenFlowTextRecordId"
language="xml"
message="Declare recordId as a record variable of the object, not as text"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A screen Flow launched from a record receives the whole record when its input variable recordId is a record variable of the object, so it reads the record fields with no Get Records element.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Flow[processType = 'Flow']/variables[lower-case(name) = 'recordid'][isInput = 'true'][dataType = 'String']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: FLOW NAMING -->
<!-- ============================================================ -->
<!--
A Flow's API name is the name of its file, and it cannot be changed
after the Flow is first saved. The developer types the label, and Flow
Builder makes the API name from it: each dash and each space becomes
an underscore. So the rule reads the file name, and finds its parts by
what each must be, not by counting underscores, because the words
inside a part may be joined or separated.
The kind of Flow comes from processType and start/triggerType. An
autolaunched Flow with a record variable named record is a trigger
action of the Trigger Actions Framework, which requires that variable.
Whether an autolaunched Flow is a subflow is not in its file, so either
of its codes is accepted. The object part is compared with start/object
without its suffix, its namespace, its underscores, or its case; a Flow
with no start/object may leave the object out. Process Builder
processes and the other kinds of Flow are not read.
-->
<rule
name="FlowNameConvention"
language="xml"
message="Name this Flow with a project, application, or business unit code, the object, the kind code, and a purpose, in that order"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A Flow's name has four parts: a code for the project, application, or business unit that owns the Flow, the object the Flow works on, a code for the kind of Flow, and the purpose, in that order. The object part may be left out when the Flow has no single object. The kind code must agree with how the Flow starts. The businessCodePattern and kindCodes properties hold the codes.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/Flow[
let $pt := string(processType), $tt := string(start/triggerType),
$kind := if ($pt = 'Flow') then 'screen'
else if ($pt = 'Orchestrator') then 'orchestration'
else if ($pt != 'AutoLaunchedFlow') then ''
else if ($tt = 'RecordBeforeSave') then 'beforeSave'
else if ($tt = 'RecordAfterSave') then 'afterSave'
else if ($tt = 'RecordBeforeDelete') then 'beforeDelete'
else if ($tt = 'Scheduled') then 'scheduled'
else if ($tt = 'PlatformEvent') then 'platformEvent'
else if ($tt != '') then ''
else if (variables[name = 'record'][dataType = 'SObject']) then 'triggerAction'
else 'autolaunched',
$codes := tokenize(substring-after(($kindCodes[starts-with(., concat($kind, '='))])[1], '='), '\|'),
$name := replace(pmd:fileName(), '^(.*/)?([^/]+?)(\.[A-Za-z]+-meta)?\.xml$', '$2'),
$object := lower-case(replace(replace(replace(string(start/object), '__[A-Za-z]+$', ''), '^[A-Za-z0-9]+__', ''), '_', '')),
$rest := replace($name, concat('^(?:', $businessCodePattern, ')_'), '')
return exists($codes)
and not(
matches($name, concat('^(?:', $businessCodePattern, ')_'))
and (some $c in $codes satisfies
matches($rest, concat('^(.+_)?', $c, '_.+$'))
and ($object = ''
or lower-case(replace(replace($rest, concat('^(.+?)_', $c, '_.+$'), '$1'), '_', '')) = $object))
)
]
]]></value>
</property>
<property
name="businessCodePattern"
type="String"
value="[A-Z]{2,5}"
description="A regular expression for the project, application, or business unit code that starts every Flow name. List your own codes, such as SAL|SER|FUL. Use no capturing group." />
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="kindCodes"
type="List[String]"
value="screen=SCR,beforeSave=Before,afterSave=After,beforeDelete=BeforeDelete|BeforeDel,scheduled=SCH,platformEvent=EVT,triggerAction=TAB|TAA,autolaunched=AL|SUB,orchestration=ORCH"
description="The code for each kind of Flow, as kind=code. Separate two codes for one kind with a vertical bar. The kinds are screen, beforeSave, afterSave, beforeDelete, scheduled, platformEvent, triggerAction, autolaunched, and orchestration." />
</properties>
</rule>
<!--
Flow Builder makes an element's API name from its label, so the verb
that starts the label starts the name. A verb may be followed by an
underscore or by a capital letter, so that GetAccount and Get_Account
both pass. An element type that has no entry in the verbs property is
not read.
-->
<rule
name="FlowElementVerb"
language="xml"
message="Start this element name with the verb for its type, such as Get for a Get Records element"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The name of a Flow element starts with the verb for its type, so that a reader knows what the element does from its name. The verbs property holds the verb or verbs for each element type.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/Flow[not(processType = ('Workflow', 'InvocableProcess', 'CustomEvent'))]/*[
let $type := local-name(),
$words := tokenize(substring-after(($verbs[starts-with(., concat($type, '='))])[1], '='), '\|')
return exists($words) and not(some $w in $words satisfies matches(string(name), concat('^', $w, '(_|[A-Z0-9]|$)')))
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="verbs"
type="List[String]"
value="recordLookups=Get,recordCreates=Create,recordUpdates=Update,recordDeletes=Delete,assignments=Set|Add|Remove,loops=Loop,collectionProcessors=Sort|Filter,transforms=Transform"
description="The verb for each element type, as type=verb, where the type is the element's tag in the Flow file. Separate two verbs for one type with a vertical bar." />
</properties>
</rule>
<rule
name="FlowDecisionQuestion"
language="xml"
message="Write this Decision label as a question that ends with a question mark"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The label of a Decision element is the question that the Decision answers, and it ends with a question mark, so that each outcome reads as an answer.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/Flow[not(processType = ('Workflow', 'InvocableProcess', 'CustomEvent'))]/decisions/label[not(ends-with(normalize-space(.), '?'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
A resource has a name and no label, so the developer types its name.
A variable is a record when its dataType is SObject, and a collection
when isCollection is true, whatever its data type. A choice and a
choice set share one prefix. A prefix may be followed by an
underscore or by a capital letter or digit, so that varRenewalDate and
var_Renewal_Date both pass.
-->
<rule
name="FlowResourcePrefix"
language="xml"
message="Start this resource name with the prefix for its type, such as var, rec, or col"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The name of a Flow resource starts with a prefix for its type, so that a reader can tell a single value, a record, and a collection apart, and so that a resource never takes the name of an element. The prefixes property holds the prefixes, and the exemptNames property holds the names that the platform or a framework requires.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/Flow[not(processType = ('Workflow', 'InvocableProcess', 'CustomEvent'))]/*[local-name() = ('variables', 'formulas', 'constants', 'textTemplates', 'choices', 'dynamicChoiceSets')][not(name = $exemptNames)][
let $type := local-name(),
$kind := if ($type = 'variables') then (if (isCollection = 'true') then 'collection' else if (dataType = 'SObject') then 'record' else 'variable')
else if ($type = 'formulas') then 'formula'
else if ($type = 'constants') then 'constant'
else if ($type = 'textTemplates') then 'textTemplate'
else 'choice',
$found := tokenize(substring-after(($prefixes[starts-with(., concat($kind, '='))])[1], '='), '\|')
return exists($found) and not(some $p in $found satisfies matches(string(name), concat('^', $p, '(_|[A-Z0-9])')))
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="prefixes"
type="List[String]"
value="variable=var,record=rec,collection=col,formula=frm,constant=con,textTemplate=tt,choice=ch"
description="The prefix for each type of resource, as type=prefix. The types are variable, record, collection, formula, constant, textTemplate, and choice. Separate two prefixes for one type with a vertical bar." />
<property
name="exemptNames"
type="List[String]"
value="recordId,ids,record,recordPrior"
description="Resource names that the rule does not read, because the platform or a framework requires them." />
</properties>
</rule>
</ruleset>
The custom rules for XML metadata: permissions, the sharing model, names, descriptions, fields, and org settings.
Metadata
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="DevSecOps Reference Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
Custom PMD rules for XML metadata, written in XPath: permissions, the
sharing model, names, descriptions, fields, and org settings. The other
custom rules are in pmd-custom-rules-apex.xml,
pmd-custom-rules-markup.xml, pmd-custom-rules-flow.xml, and, for
JavaScript, pmd-custom-rules-javascript.xml. The stock PMD rules this
reference adopts are in pmd-stock-rules.xml and, for JavaScript, in
pmd-stock-rules-javascript.xml. Adjust priorities and suppressions to
match your organisation's risk posture and coding standards.
</description>
<!--
Rule messages contain no apostrophes. PMD formats a message with Java's
MessageFormat, which treats a single apostrophe as a quoting character
and drops it, so "class's" would be printed as "classs".
-->
<exclude-pattern>.*/.sfdx/.*</exclude-pattern>
<exclude-pattern>.*/.sf/.*</exclude-pattern>
<exclude-pattern>.*/node_modules/.*</exclude-pattern>
<!-- ============================================================ -->
<!-- CUSTOM RULES: PERMISSIONS -->
<!-- ============================================================ -->
<rule
name="ModifyOrViewAllData"
language="xml"
message="ViewAllData and ModifyAllData grant unrestricted record access - restrict to System Administrator only"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Profiles and Permission Sets must not grant ViewAllData or ModifyAllData. An integration user may need one of them; that grant is approved by senior reviewers.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(PermissionSet | Profile)/userPermissions[name = ('ModifyAllData', 'ViewAllData') and enabled = 'true']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="violationSuppressXPath"
value="ancestor::Profile[not(custom/text[@Text = 'true'])]" />
</properties>
</rule>
<rule
name="ModifyOrViewAllRecordsCustom"
language="xml"
message="Use sharing rules to grant record access rather than View All or Modify All on custom objects"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Profiles and Permission Sets should not grant View All or Modify All Records on custom objects.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(Profile | PermissionSet)/objectPermissions[(modifyAllRecords = 'true' or viewAllRecords = 'true') and contains(object, '__')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="violationSuppressXPath"
value="ancestor::Profile[not(custom/text[@Text = 'true'])]" />
</properties>
</rule>
<rule
name="ModifyOrViewAllRecordsStandard"
language="xml"
message="View All or Modify All Records on standard objects bypasses the sharing model - blocking violation"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Profiles and Permission Sets must not grant View All or Modify All Records on standard objects.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(Profile | PermissionSet)/objectPermissions[(modifyAllRecords = 'true' or viewAllRecords = 'true') and not(contains(object, '__'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="violationSuppressXPath"
value="ancestor::Profile[not(custom/text[@Text = 'true'])]" />
</properties>
</rule>
<rule
name="ManageUsers"
language="xml"
message="Manage Users permissions should be restricted to the System Administrator profile"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Manage Users is a restricted permission.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(Profile | PermissionSet)/userPermissions[matches(name, '^Manage.*Users$') and enabled = 'true']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="violationSuppressXPath"
value="ancestor::Profile[not(custom/text[@Text = 'true'])]" />
</properties>
</rule>
<rule
name="ViewSetupProfile"
language="xml"
message="ViewSetup should be restricted to the System Administrator profile"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>ViewSetup is a restricted permission that must not appear in custom profiles.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Profile/userPermissions[name = 'ViewSetup' and enabled = 'true']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="violationSuppressXPath"
value="ancestor::Profile[not(custom/text[@Text = 'true'])]" />
</properties>
</rule>
<rule
name="ViewSetupPermissionSet"
language="xml"
message="ViewSetup must not be granted via a Permission Set"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>ViewSetup must not appear in Permission Sets.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//PermissionSet/userPermissions[name = 'ViewSetup' and enabled = 'true']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: SHARING MODEL -->
<!-- ============================================================ -->
<rule
name="NonPrivateCustomObjectSharing"
language="xml"
message="Custom objects must use Private or a platform-controlled org-wide default sharing model"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Custom objects must use Private, or a platform-controlled sharing model (ControlledByParent and the other Controlled* values), for both the internal and the external org-wide default.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//CustomObject[contains(pmd:fileName(), '__')][(externalSharingModel | sharingModel)[not(. = 'Private' or starts-with(., 'Controlled'))]]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="NonPrivateStandardObjectSharing"
language="xml"
message="Standard objects must use Private or a platform-controlled org-wide default sharing model"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Standard objects must use Private, or a platform-controlled sharing model (ControlledByParent, ControlledByCampaign, ControlledByLeadOrContact), for both the internal and the external org-wide default.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//CustomObject[not(contains(pmd:fileName(), '__'))][(externalSharingModel | sharingModel)[not(. = 'Private' or starts-with(., 'Controlled'))]]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: METADATA NAMING -->
<!-- ============================================================ -->
<!--
The first letter of a component's name and label is a capital. Two
generic rules take the metadata types to read from a property, by the
root element of the file (Flow, CustomObject, and CustomField by
default; add a type to the list to cover it too). The name rule reads
the fullName element where the file has one, as a field's does, and
the file's name otherwise, as a Flow's and an object's do in source
format, up to the type suffix. A label may start with a digit. The
rest of the naming standard (the underscore convention, the Pascal
case exception for an object that already uses it, and consistency
among similar fields) is for reviewers: no rule reads an object's
other fields.
-->
<rule
name="MetadataNameStartsWithCapital"
language="xml"
message="Start the API name with a capital letter"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The API name of a Flow, a custom object, or a custom field starts with a capital letter, in a file of its own or inside an object file; the types property names the metadata types the rule reads. The name is the fullName element where the file has one, and the file's name otherwise. A name with a namespace prefix belongs to a managed package, and an org cannot change it, so it is not reported. A name is hard to change once the component is referenced, so the rule blocks.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(matches(string((fullName, replace(pmd:fileName(), '^(.*/)?([^/]+?)(\.[A-Za-z]+-meta)?\.xml$', '$2'))[1]), '^[A-Z]|^[A-Za-z0-9]+__.*[^a-z]'))] | /CustomObject[$types = 'CustomField']/fields[not(matches(string(fullName), '^[A-Z]|^[A-Za-z0-9]+__.*[^a-z]'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="Flow,CustomObject,CustomField"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<rule
name="MetadataLabelStartsWithCapital"
language="xml"
message="Start the label with a capital letter"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The label of a Flow, a custom object, or a custom field starts with a capital letter or a digit, in a file of its own or inside an object file; the types property names the metadata types the rule reads. A label can be changed at any time, so the rule reports and does not block.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types]/label[not(matches(., '^[A-Z0-9]'))] | /CustomObject[$types = 'CustomField']/fields/label[not(matches(., '^[A-Z0-9]'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="Flow,CustomObject,CustomField"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
A checkbox field is a CustomField whose type is Checkbox. The rule
reads custom fields only (a name that ends in __c), and not a field of
a managed package, whose name has a namespace prefix. The negative
prefix is matched without regard to case, and with or without an
underscore between its letters, so that Is_Not_Active__c and
ISNOTACTIVE__c are reported. The negative prefix is reported only
where an underscore, a capital letter, or a digit follows it, which
starts a new word, so that Is_Notified__c is not reported.
-->
<rule
name="CheckboxFieldNamePrefix"
language="xml"
message="Start this checkbox field name with ${prefix} and never with ${negativePrefix}, so that it reads as a yes-or-no question"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The API name of a custom checkbox field starts with the prefix property, so that it reads as a yes-or-no question about the record, and never with the negativePrefix property, which would make every check of the field a double negative.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/CustomField[type = 'Checkbox'][
let $name := string((fullName, replace(pmd:fileName(), '^(.*/)?([^/]+?)(\.[A-Za-z]+-meta)?\.xml$', '$2'))[1]),
$negative := string-join(for $c in string-to-codepoints($negativePrefix) return concat('[', upper-case(codepoints-to-string($c)), lower-case(codepoints-to-string($c)), ']_?'), '')
return ends-with($name, '__c') and not(matches($name, '^[A-Za-z0-9]+__.+__c$'))
and (not(matches($name, concat('^', $prefix, '(_|[A-Z0-9])'))) or matches($name, concat('^', $negative, '(_|[A-Z0-9])')))
]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="negativePrefix"
type="String"
value="IsNot"
description="The prefix that a checkbox field name never starts with, matched without regard to case." />
<property
name="prefix"
type="String"
value="Is"
description="The prefix that every checkbox field name starts with." />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: METADATA DOCUMENTATION -->
<!-- ============================================================ -->
<rule
name="MetadataDescriptionCustomField"
language="xml"
message="Add a description of more than 10 characters to explain this custom field"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Every custom field must have a description of more than 10 characters.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(CustomField | CustomObject/fields)[contains(fullName, '__')][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="MetadataDescriptionStandardField"
language="xml"
message="Add a description of more than 10 characters to explain how this standard field is used here"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Every standard field should carry a description of more than 10 characters. The description of a standard field can be set through the API.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(CustomField | CustomObject/fields)[not(contains(fullName, '__'))][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="MetadataDescriptionCustomObject"
language="xml"
message="Add a description of more than 10 characters to explain this custom object"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Every custom object must have a description of more than 10 characters.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//CustomObject[contains(pmd:fileName(), '__')][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="MetadataDescriptionPermission"
language="xml"
message="Add a description of more than 10 characters to explain what access this metadata grants"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>All Permission Sets, Profiles, Custom Permissions, Permission Set Groups, Muting Permission Sets, and User Access Policies must have a description of more than 10 characters.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(PermissionSet | CustomPermission | PermissionSetGroup | MutingPermissionSet)/label[normalize-space() != '' and (not(../description) or string-length(normalize-space(../description)) <= 10)] | //Profile[custom = 'true'][not(description) or string-length(normalize-space(description)) <= 10] | //UserAccessPolicy[not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="MetadataDescriptionFlow"
language="xml"
message="Add a description of more than 10 characters to explain the purpose of this Flow or Flow component"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>All Flows, Flow tests, Flow categories, and Flow value maps must have a description of more than 10 characters.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(Flow | FlowTest | FlowValueMap)/label[normalize-space() != '' and (not(../description) or string-length(normalize-space(../description)) <= 10)] | //FlowCategory[not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Lightning and Visualforce components, apps, tabs, pages, actions, static
resources, and other parts of the user interface and its code.
-->
<rule
name="MetadataDescriptionUserInterface"
language="xml"
message="Add a description of more than 10 characters to explain what this user interface component does"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A user interface or code component of the metadata types in the types property carries a description of more than 10 characters, so that a reader learns what it does and where it is used.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="ApexPage,ApexComponent,AuraDefinitionBundle,LightningComponentBundle,LightningMessageChannel,LightningTypeBundle,UIBundle,CustomApplication,CustomTab,FlexiPage,QuickAction,CustomPageWebLink,StaticResource,PlatformCachePartition"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
The parts of the data model beyond objects and fields, which have
rules of their own, and the processes that run on its records.
-->
<rule
name="MetadataDescriptionCore"
language="xml"
message="Add a description of more than 10 characters to explain what this part of the data model or process is for"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A component of the data model, or a process on it, of the metadata types in the types property carries a description of more than 10 characters, so that a reader learns its purpose without reading everything that uses it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="BusinessProcess,RecordType,WebLink,ValidationRule,FieldSet,GlobalValueSet,DataCategoryGroup,ReportType,ApprovalProcess,ActionPlanTemplate,CustomNotificationType"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
Duplicate rules and matching rules. Matching rules have no file of their
own: they are elements inside one MatchingRules file for each object,
so the childElements property names them, and each is read where it
sits.
-->
<rule
name="MetadataDescriptionData"
language="xml"
message="Add a description of more than 10 characters to explain what this data quality rule checks"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A data quality component of the metadata types in the types property, and each element named in the childElements property, carries a description of more than 10 characters, so that a reader learns what it checks and why.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
|
/*/*[local-name() = $childElements][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="childElements"
type="List[String]"
value="matchingRules"
description="Elements that hold a component inside a file of another type. Each is read where it sits." />
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="DuplicateRule"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
Groups, sharing rules, sharing sets, restriction rules, and transaction
security policies. Most sharing rules have no file of their own: they
are elements inside one SharingRules file for each object, and every
element of that file is a sharing rule, so the rule reads every one of
them, whatever its kind, including a kind added later.
-->
<rule
name="MetadataDescriptionSharing"
language="xml"
message="Add a description of more than 10 characters to explain what access this component grants or limits"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A sharing or access component of the metadata types in the types property, and every sharing rule in a SharingRules file, carries a description of more than 10 characters, so that a reader learns what access it grants or limits, and why.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
|
/SharingRules/*[not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="Group,AccountRelationshipShareRule,SharingSet,RestrictionRule,FieldRestrictionRule,TransactionSecurityPolicy"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
The metadata types through which the org reaches other systems, or
other systems reach the org.
-->
<rule
name="MetadataDescriptionIntegration"
language="xml"
message="Add a description of more than 10 characters to explain what this integration component connects and why"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>An integration component of the metadata types in the types property carries a description of more than 10 characters, so that a reader learns what it connects to and why.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="CspTrustedSite,RemoteSiteSetting,NamedCredential,ExternalCredential,ExternalClientApplication,ConnectedApp,ExternalAuthIdentityProvider,ExternalServiceRegistration,RegisteredExternalService,CatalogedApi,CatalogedApiVersion,IntegrationProviderDef,InboundNetworkConnection,OutboundNetworkConnection,PublicKeyCertificate,PublicKeyCertificateSet,OauthCustomScope,OauthTokenExchangeHandler"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
Agents, their topics, actions, planners, and prompt templates, and the
Einstein models and strategies. An agent reads the descriptions of its
topics and actions as instructions, so a description here changes how
the agent behaves as well as telling a reader what the component is.
-->
<rule
name="MetadataDescriptionEinstein"
language="xml"
message="Add a description of more than 10 characters to explain what this agent or Einstein component does"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>An agent or Einstein component of the metadata types in the types property carries a description of more than 10 characters, so that a reader, and an agent that reads it, learns what the component does.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="Bot,BotBlock,BotTemplate,GenAiPlannerBundle,GenAiPlanner,GenAiPlugin,GenAiFunction,GenAiPromptTemplate,AiEvaluationDefinition,MlDomain,RecommendationStrategy,MLPredictionDefinition,AIScoringModelDefinition,DiscoveryAIModel"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
The other metadata types that carry a description: business rules,
service, collaboration, sites, territories, Data Cloud, and the
types of other products. A type whose feature the org does not use is
simply never found.
-->
<rule
name="MetadataDescriptionComponent"
language="xml"
message="Add a description of more than 10 characters to explain what this component is for"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A component of the metadata types in the types property carries a description of more than 10 characters, so that a reader learns its purpose without reading everything that uses it.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="DecisionTable,DecisionTableDatasetLink,DecisionMatrixDefinition,ExpressionSetDefinition,ExpressionSetDefinitionVersion,ExpressionSetMessageToken,EntitlementProcess,MilestoneType,ServiceProcess,Skill,MessagingChannel,ConversationMessageDefinition,LiveChatSensitiveDataRule,SvcCatalogItemDef,SvcCatalogFulfillmentFlow,BriefcaseDefinition,TimeSheetTemplate,ModerationRule,PostTemplate,CustomFeedFilter,ChatterExtension,Network,DigitalExperienceBundle,ManagedContentType,UserCriteria,CustomSite,Audience,Territory2,Territory2Model,Territory2Type,DataStreamDefinition,DataConnector,DataSourceBundleDefinition,DataPackageKitDefinition,MktCalcInsightObjectDef,ActivationPlatform,AccountPlanObjMeasCalcDef,ActionableListDefinition,AdvAccountForecastSet,AppFrameworkTemplateBundle,BatchCalcJobDefinition,BatchProcessJobDefinition,BusinessProcessGroup,ChoiceList,ClauseCatgConfiguration,ContentTypeBundle,ContextDefinition,DisclosureDefinition,DisclosureDefinitionVersion,DisclosureType,DocumentCategory,DocumentType,OcrTemplate,EnablementMeasureDefinition,EnablementProgramDefinition,FuelType,SustainabilityUom,MarketingAppExtension,MfgProgramTemplate,OnboardingDataObjectGroup,ProductAttributeSet,RecordAlertCategory,RelatedRecordAssocCriteria,StageAssignment,StageDefinition,WebStoreTemplate"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!--
Reports, dashboards, and CRM Analytics and Tableau Next assets are often
made by users in production, so the standards do not require a
description on one. A rule of its own lets an organisation that keeps
them in version control ask for one.
-->
<rule
name="MetadataDescriptionAnalytics"
language="xml"
message="Add a description of more than 10 characters to explain what this analytics asset shows"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A report, dashboard, or other analytics asset of the metadata types in the types property carries a description of more than 10 characters, so that a reader learns what it shows and who uses it.</description>
<priority>4</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $types][not(description) or string-length(normalize-space(description)) <= 10]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="types"
type="List[String]"
value="Report,Dashboard,AnalyticSnapshot,AnalyticsDashboard,AnalyticsVisualization,AnalyticsWorkspace,WaveApplication,WaveDashboard,WaveLens,WaveDataflow,WaveDataset,WaveComponent,WaveAnalyticAssetCollection,WaveTemplateBundle,ReferencedDashboard"
description="The metadata types the rule reads, by the root element of their files. Add a type to the list to cover it too." />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: FIELD CLASSIFICATION AND DESIGN -->
<!-- ============================================================ -->
<!--
Data classification rules. complianceGroup and securityClassification are the CustomField
elements behind the Compliance Categorization and Data Sensitivity Level fields in Setup.
CustomField is the root element for standard fields as well, so all fields are checked.
-->
<rule
name="FieldComplianceCategory"
language="xml"
message="Set the Compliance Categorization on this field"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Every field, standard and custom, must carry a Compliance Categorization (the complianceGroup element: PII, HIPAA, GDPR, PCI, COPPA, CCPA, or None). Fields in the metadata-API object shape (CustomObject/fields) are covered too.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(CustomField | CustomObject/fields)[not(complianceGroup)]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="FieldDataSensitivity"
language="xml"
message="Set the Data Sensitivity Level on this field"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Every field, standard and custom, must carry a Data Sensitivity Level (the securityClassification element: Public, Internal, Confidential, Restricted, or MissionCritical).</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(CustomField | CustomObject/fields)[not(securityClassification)]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="FieldPiiMinimumSensitivity"
language="xml"
message="A field categorised as PII or HIPAA must have a Data Sensitivity Level of Confidential or higher"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A field whose Compliance Categorization includes PII or HIPAA must have a Data Sensitivity Level of Confidential, Restricted, or MissionCritical. complianceGroup is a semicolon-separated list, so it is matched with a regular expression.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(CustomField | CustomObject/fields)[complianceGroup[matches(., 'PII|HIPAA')]][not(securityClassification = ('Confidential', 'Restricted', 'MissionCritical'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<rule
name="MasterDetailReparentable"
language="xml"
message="This master-detail field does not allow reparenting - confirm that this is the choice your organisation has made"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A master-detail field with reparentableMasterDetail set to false, or left out, which has the same effect, locks every child record to the parent it was created under; a data fix then needs a delete and re-create. Whether to allow reparenting is a decision for each organisation, which these standards do not make; the rule reports the field so that the choice is seen. Where reparenting is allowed, control who may reparent through field-level security and validation rules.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(CustomField | CustomObject/fields)/reparentableMasterDetail[. = 'false'] | //(CustomField | CustomObject/fields)[type = 'MasterDetail'][not(reparentableMasterDetail)]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: METADATA QUALITY -->
<!-- ============================================================ -->
<!--
Reports the URL of a weblink, a custom button or link, that holds the
absolute URL of a Salesforce org, with the host pattern and the
allowedSubdomains property of AvoidAbsoluteSalesforceUrl, the Apex rule:
a link to one of the public Salesforce websites is not reported. Weblinks
from namespaced packages are skipped, because an org cannot change them.
-->
<rule
name="NoHardcodedWeblinkURLs"
language="xml"
message="Do not hard-code a Salesforce URL in a weblink - use a relative URL"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A weblink URL that holds the absolute URL of a Salesforce org is right in one org only, because every sandbox and every production org has a host name of its own. Use a relative URL. The allowedSubdomains property holds the subdomains of salesforce.com that may be written out, and each organisation adds its own. Weblinks from namespaced packages are skipped; an org cannot change them.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//(WebLink | CustomPageWebLink | CustomObject/webLinks)[not(contains(fullName, '__'))]/url[matches(., 'https?://([a-z0-9-]+\.)+((salesforce|force|visualforce|salesforce-sites|salesforce-setup|cloudforce)\.com|my\.site\.com)', 'i')][not(some $s in $allowedSubdomains satisfies matches(., concat('https?://', $s, '\.salesforce\.com'), 'i'))]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="allowedSubdomains"
type="List[String]"
value="www,help,developer,trailhead,status,trust,appexchange"
description="The subdomains of salesforce.com whose URLs may be written out, each as the part of the host before .salesforce.com" />
</properties>
</rule>
<!--
Reports a metadata file at an API version below the floor in the
minimumApiVersion property.
Salesforce Code Analyzer has a rule of its own for this,
AvoidOldSalesforceApiVersions, which reports any API version three or
more years old. Its three years is fixed and cannot be set, and its
floor rises with each Salesforce release. The floor set here is the one
that rule applied in October 2026.
This is the rule to change for a different floor: raise the property
as releases pass, or set the floor your organisation has agreed. The
property is set in this file because Salesforce Code Analyzer does not
pass rule properties from code-analyzer.yml to PMD rules.
-->
<rule
name="BumpApiVersion"
language="xml"
message="This metadata is below API version ${minimumApiVersion} - raise its API version"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A metadata file at an API version below the floor must be raised. The minimumApiVersion property holds the floor, and each organisation sets its own.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//apiVersion[number(.) < $minimumApiVersion]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="minimumApiVersion"
type="Integer"
value="60"
description="The lowest API version a metadata file may have" />
</properties>
</rule>
<!--
Governance rules on whole metadata types. The list is policy, not platform facts:
each organisation sets its own. ProhibitedMetadataType, below, carries the types
this reference recommends. It holds its list in its metadataTypes property, where a
type is named by the root element of its file, so the list is changed without
touching the XPath.
-->
<!--
Not adopted: MetadataRequiresApproval. Every type it reported is Tier 1, and the
architect team already reviews every Tier 1 file through CODEOWNERS and the Metadata
Risk Tier workflow. It is kept here, commented out, as a record. An organisation that
wants a developer to see the warning in the editor, before the pull request, can
restore it (Appendix F: Custom Rules Not Adopted).
<rule
name="MetadataRequiresApproval"
language="xml"
message="This metadata type (${metadataTypes}, or a settings file) requires architect approval before it is promoted"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Some metadata types change org-wide behaviour or record access for external users. A file of a type named in the metadataTypes property needs an architect's approval before it is promoted; the rule marks the file so that the review cannot miss it, and each organisation names its own types. Every settings file is reported as well, matched by file name because each settings type has its own root element.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $metadataTypes] | /*[matches(pmd:fileName(), '\.settings(-meta)?\.xml$')]
]]></value>
</property>
<property name="defaultNsUri" value="http://soap.sforce.com/2006/04/metadata" />
<property
name="metadataTypes"
type="List[String]"
value="Audience,SharingSet"
description="The metadata types that need an architect's approval, each by the name of the root element of its file" />
</properties>
</rule>
-->
<!--
Tier 0, prohibited metadata. The metadataTypes property holds the
Tier 0 metadata types of the metadata risk matrix, each by the name of
the root element of its file, and the rule reports every file of a
listed type. The list is policy, as the note above says: keep it in
step with the other Tier 0 lists (the ignore files, the risk matrix
file, and the push ruleset). An organisation that makes another type
Tier 0, as some do with RemoteSiteSetting, adds its name here.
The rule reads what a file contains and not where it is, so it also
reports a Tier 0 file saved under another name or in another folder.
It reads XML only: the .crt file of a certificate and the .scf file of
an s-control are not XML, and the path checks cover them.
-->
<rule
name="ProhibitedMetadataType"
language="xml"
message="This metadata type is Tier 0 (prohibited: ${metadataTypes}) and must not be committed to version control"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Tier 0 metadata is managed in each org and never enters version control. The rule reports every metadata file whose type is named in its metadataTypes property, and each organisation names its own types.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $metadataTypes]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="metadataTypes"
type="List[String]"
value="FlowDefinition,Certificate,ExtlClntAppGlobalOauthSettings,Scontrol"
description="The Tier 0 metadata types, each by the name of the root element of its file" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: INTEGRATION ENDPOINTS -->
<!-- ============================================================ -->
<!--
Reports an IPv4 address in the metadata that says where the org
connects to. The metadataTypes property holds the metadata types the
rule reads, each by the name of the root element of its file; add a
type to have its files read too, such as CustomMetadata where records
hold endpoints. In a file of a listed type the rule reads every
element that holds a value, and reports a value that is an address,
alone or with a port, or a URL whose host is one. An address inside
other text, such as a version number in a path or a description, is
not reported.
-->
<rule
name="AvoidHardcodedIpAddressMetadata"
language="xml"
message="Do not hard-code an IP address in this metadata - use a host name"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A value in integration metadata that is an IPv4 address, or a URL whose host is one, ties the org to one network address. Use a host name. The rule reads the files whose type is named in its metadataTypes property, and each organisation may add its own types.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/*[local-name() = $metadataTypes]//*[not(*)][matches(normalize-space(.), '(^|://)((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])($|[:/?#])')]
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
<property
name="metadataTypes"
type="List[String]"
value="NamedCredential,RemoteSiteSetting,CspTrustedSite,CorsWhitelistOrigin,ExternalDataSource,AuthProvider,Workflow,WorkflowOutboundMessage"
description="The metadata types whose values are read, each by the name of the root element of its file" />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: SECRETS IN METADATA -->
<!-- ============================================================ -->
<!--
A sample rule, for one kind of secret: a Salesforce session ID, which
is the 15-character or 18-character org ID, an exclamation mark, and a
token. The rule reads the text of every element of every metadata
file, descriptions included, and reports a value that matches the
secretPattern property. Copy the rule once for each vendor that
integrates with the org, name the copy for the vendor, such as
AvoidHardcodedSecretStripe, and set its secretPattern to the form of
that vendor's keys. Write a boundary as a character class, because
XPath has no \b. Lightning Flow Scanner's own pattern reads an
18-character org ID only, and a session ID starts with the
15-character form.
Adapted from HardcodedSecret of Lightning Flow Scanner,
https://github.com/Flow-Scanner/lightning-flow-scanner, under its MIT licence:
Copyright (c) 2020
https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/LICENSE.md
-->
<rule
name="AvoidHardcodedSecretSalesforce"
language="xml"
message="Remove this Salesforce session ID - keep secrets out of metadata"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>A secret written into metadata is read by everyone who can read the repository, and it stays in the history after the file is corrected. Keep a secret in an External Credential or a Named Credential, and read it from there. The secretPattern property holds the form of one kind of secret; copy the rule once for each vendor.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//*[not(*)][matches(., $secretPattern)]
]]></value>
</property>
<property
name="secretPattern"
type="String"
value="(^|[^A-Za-z0-9_])00D[a-zA-Z0-9]{12}([a-zA-Z0-9]{3})?![a-zA-Z0-9._]{80,}"
description="A regular expression for one kind of secret. As shipped, a Salesforce session ID. Set it to the form of another vendor's keys in a copy of the rule." />
</properties>
</rule>
<!-- ============================================================ -->
<!-- CUSTOM RULES: ORG SETTINGS -->
<!-- ============================================================ -->
<!--
Lightning Web Security is switched on, with the stricter Content
Security Policy that it depends on. The settings are lockerServiceNext
(true is Lightning Web Security, false is Lightning Locker) and
lockerServiceCSP, both in the session settings of the Security settings
file, and the rule reports either when it is false. Lightning Out, which
Lightning Web Security does not support, is not a setting: the rules
NoLightningOutApplicationAura and NoLightningOutInVisualforce report it in
the code, with the same message. A settings file in version control is
often partial, so a file that leaves an element out says nothing about
the org and is not reported.
-->
<rule
name="LightningWebSecurityNotEnabled"
language="xml"
message="Switch on Lightning Web Security, with its stricter Content Security Policy - set lockerServiceNext and lockerServiceCSP to true in Session Settings"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>Lightning Web Security must be switched on, with its stricter Content Security Policy: lockerServiceNext and lockerServiceCSP in the session settings of the Security settings file are true. The rule reports either element when it is false. A settings file that leaves an element out is not reported.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/SecuritySettings/sessionSettings/*[self::lockerServiceNext or self::lockerServiceCSP][. = 'false']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
Processes and Flows are deployed as active. The setting is
enableFlowDeployAsActiveEnabled in the Flow settings file, and the
rule reports it when it is false. As above, a file that leaves the
element out is not reported.
-->
<rule
name="FlowDeployAsActiveNotEnabled"
language="xml"
message="Deploy processes and flows as active is switched off - switch it on in Process Automation Settings"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>The org must deploy processes and Flows as active: enableFlowDeployAsActiveEnabled in the Flow settings file is true. The rule reports the element when it is false. A settings file that leaves the element out is not reported.</description>
<priority>3</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/FlowSettings/enableFlowDeployAsActiveEnabled[. = 'false']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
<!--
No new list custom setting is created. A custom setting's definition
is its object file, and customSettingsType in it is List or Hierarchy.
The rule reports each definition whose type is List, so that an
existing list custom setting is seen at each change, and a new value
goes in a custom metadata type.
-->
<rule
name="NoListCustomSetting"
language="xml"
message="A list custom setting is not created - keep org-wide values in a custom metadata type, and user-specific values in a hierarchy custom setting"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<description>No new list custom setting is created: org-wide values go in a custom metadata type, and user-specific or profile-specific values in a hierarchy custom setting. The rule reports each custom setting definition whose customSettingsType is List.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
/CustomObject/customSettingsType[. = 'List']
]]></value>
</property>
<property
name="defaultNsUri"
value="http://soap.sforce.com/2006/04/metadata" />
</properties>
</rule>
</ruleset>
The custom rules for the JavaScript of Lightning web components and Aura components.
JavaScript
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="DevSecOps Reference Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
Custom PMD rules for the JavaScript of Lightning web components and Aura
components, written in XPath. The other custom rules are in
pmd-custom-rules-apex.xml, pmd-custom-rules-markup.xml,
pmd-custom-rules-metadata.xml, and pmd-custom-rules-flow.xml, and the
stock PMD rules this reference adopts for JavaScript are in
pmd-stock-rules-javascript.xml. Adjust priorities and suppressions to
match your organisation's risk posture and coding standards.
</description>
<!--
Rule messages contain no apostrophes. PMD formats a message with Java's
MessageFormat, which treats a single apostrophe as a quoting character
and drops it.
-->
<exclude-pattern>.*/.sfdx/.*</exclude-pattern>
<exclude-pattern>.*/.sf/.*</exclude-pattern>
<exclude-pattern>.*/node_modules/.*</exclude-pattern>
<!-- ============================================================ -->
<!-- CUSTOM RULES: JAVASCRIPT SECURITY -->
<!-- ============================================================ -->
<!--
Reports an assignment to document.cookie or window.document.cookie
that has no string part, in a literal or in a template literal, with
the Secure attribute. A cookie string kept in a variable is not
followed, so a variable is reported even when the string it holds has
the attribute.
-->
<rule
name="RequireSecureCookie"
language="ecmascript"
message="Set every cookie with the Secure attribute"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>A cookie set without the Secure attribute can be sent over a connection that is not encrypted, where it can be read. Set every cookie with the Secure attribute.</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//Assignment[
PropertyGet[1][Name[1][@Identifier = 'document']][Name[2][@Identifier = 'cookie']]
or PropertyGet[1][PropertyGet[Name[1][@Identifier = 'window']][Name[2][@Identifier = 'document']]][Name[1][@Identifier = 'cookie']]
][not(.//StringLiteral[matches(@Value, ';\s*secure\s*(;|$)', 'i')] | .//TemplateCharacters[matches(@Value, ';\s*secure\s*(;|$)', 'i')])]
]]></value>
</property>
</properties>
</rule>
</ruleset>