Skip to content

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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp;. 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 &gt;= y, not !(x &lt; 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 &gt; 0, not if (x &gt; 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 &amp;&amp; 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 &amp;&amp; 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() &gt; 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 (!=, &lt;&gt;, 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>