Skip to content

Appendix F: Reference PMD Ruleset for Salesforce

This appendix provides a reference pmd-ruleset.xml file for use as the starting point for a Salesforce project's PMD configuration. The ruleset is organized into two distinct categories, each serving a different governance purpose:

Part 1: Apex Code Quality Custom Rules covers some custom rules for Apex. This, combined with rules in code-analyzer.yml is the code quality layer, governing how code is written and how LWC/VF source is structured.

Part 2: Metadata Governance Custom Rules covers custom XPath rules that operate on XML metadata files rather than Apex, Visualforce, HTML, or JavaScript source. These rules enforce permission governance (restricting ViewAllData, ModifyAllData, and ReadWrite OWD on standard objects), metadata documentation requirements (descriptions on Flows, Permission Sets, and custom objects/fields), and metadata quality standards (API version floors, hardcoded URL detection). These rules are the pipeline enforcement mechanism for the Tier 1 metadata types classified in Appendix B. They belong in the pipeline architecture regardless of whether Apex code quality is in scope for a given engagement.

Both parts together form the complete pmd-ruleset.xml. The split here is for navigability; the file is deployed as a single artifact referenced in code-analyzer.yml.

All standard PMD rules and even these custom rules can be configured/overridden (e.g. severity, disabling, tagging) in the Code Analyzer configuration file.

This ruleset is a starting point, not a prescription. Rule selection, priority assignments, and suppression policies should be reviewed and adjusted to reflect each organization's risk posture, coding standards, and compliance requirements - as discussed in Section 5. Changes to the ruleset should follow the formal change process: PR, review, sign-off, and changelog entry. Be sure to communicate all changes and timelines to developers.

The custom ruleset should be stored in the project repository root or in a location explicitly referenced in code-analyzer.yml, version-controlled alongside source code, and protected by CODEOWNERS so that only the governance team (@<my-org>/admin-team) can modify it.

Custom PMD Rules (Sample)

The first groups of rules below are custom Apex and Visualforce rules. Standard PMD rules are not listed here; they can be configured in code-analyzer.yml. At this time, adding properties to standard rules in a ruleset does not affect Salesforce Code Analyzer, and overriding properties are not available in code-analyzer.xml, but may be in the future.

The following rules for Permission Sets, Profiles, Flows, Custom Objects, and other declarative metadata operate on XML metadata files and not on Apex or JavaScript source. They are the pipeline-level enforcement mechanism for the high-risk metadata types classified in Appendix B. These rules are relevant to any engagement where metadata governance is in scope, independent of the Apex code quality rules.

Please note the violation suppression, restricting these to custom Profiles and Permission Sets so false-positives do not occur when committing standard Profiles and Permission Sets to version control.

<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
    name="Salesforce Reference PMD Ruleset"
    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>
    Reference PMD ruleset for Salesforce projects.
    Covers Apex, Visualforce, XML metadata, and JavaScript (ECMAScript).
    Includes custom XPath rules for Salesforce-specific governance concerns.
    Adjust priorities and suppressions to match your organization's risk
    posture and coding standards.
  </description>

  <exclude-pattern>.*/.sfdx/.*</exclude-pattern>
  <exclude-pattern>.*/.sf/.*</exclude-pattern>
  <exclude-pattern>.*/node_modules/.*</exclude-pattern>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: VISUALFORCE                                    -->
  <!-- ============================================================ -->

  <rule name="AltTextOnImages" message="Always use an 'alt' attribute for images" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule" language="visualforce">
    <description>All images require alt text for accessibility compliance.</description>
    <priority>3</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //Element[matches(@Name,'^apex:image|img$')][count(alt)=0]
        ]]></value>
      </property>
    </properties>
  </rule>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: APEX ACCESS AND SHARING                       -->
  <!-- ============================================================ -->

  <!--
    Requires all non-test Apex methods to declare an explicit access 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>Declare an explicit access modifier on all non-test methods.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //Method[
            ModifierNode[
              @Public = false() and
              @Private = false() and
              @Protected = false() and
              @Global = false() and
              @Static = false()
            ] and
            ancestor::UserClass/ModifierNode[@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 with without sharing. Document the justification in a code comment.</description>
    <priority>3</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //UserClass/ModifierNode[@WithoutSharing=true()]
        ]]></value>
      </property>
    </properties>
  </rule>

  <rule
        name="RequireExplicitSharing"
        message="Explicitly declare with sharing, without sharing, or inherited sharing"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
        language="apex">
    <description>All Apex classes must declare an explicit sharing model. Does not apply to interfaces or enums, which cannot declare sharing.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //UserClass[
            ModifierNode[
              @WithoutSharing=false() and
              @WithSharing=false() and
              @InheritedSharing=false() and
              @Nested = false()
            ] and
            @Interface = false() and
            @Enum = 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>Test methods must not use System.runAs(UserInfo.getUserId()).</description>
    <priority>1</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>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: PERMISSION GOVERNANCE                         -->
  <!-- ============================================================ -->

  <rule
        name="ModifyOrViewAllData"
        language="xml"
        message="ViewAllData and ModifyAllData grant unrestricted record access - restrict to System Administrator only"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>Profiles and Permission Sets must not grant ViewAllData or ModifyAllData.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //(PermissionSet | Profile)/userPermissions[
            name/text[@Text='ModifyAllData' or @Text='ViewAllData'] and
            enabled/text[@Text='true']
          ]
        ]]></value>
      </property>
      <property name="violationSuppressXPath" value="//Profile/custom/text[@Text='false']" />
    </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.rule.xpath.XPathRule">
    <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 | viewAllRecords)/text[@Text='true'] and
            object/text[matches(@Text, '.*__[a-z]+')]
          ]
        ]]></value>
      </property>
      <property name="violationSuppressXPath" value="//Profile/custom/text[@Text='false']" />
    </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.rule.xpath.XPathRule">
    <description>Profiles and Permission Sets must not grant View All or Modify All Records on standard objects.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //(Profile | PermissionSet)/objectPermissions[
            (modifyAllRecords | viewAllRecords)/text[matches(@Text, 'true')] and
            object/text[matches(@Text, '[A-Z][a-zA-Z0-9]*')]
          ]
        ]]></value>
      </property>
      <property name="violationSuppressXPath" value="//Profile/custom/text[@Text='false']" />
    </properties>
  </rule>

  <rule
        name="ManageUsers"
        language="xml"
        message="Manage Users permissions should be restricted to the System Administrator profile"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>Manage Users is a restricted permission.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //(Profile | PermissionSet)/userPermissions[
            name/text[matches(@Text, 'Manage.*Users')]
          ]
        ]]></value>
      </property>
      <property name="violationSuppressXPath" value="//Profile/custom/text[@Text='false']" />
    </properties>
  </rule>

  <rule name="ViewSetupProfile" language="xml" message="ViewSetup should be restricted to the System Administrator profile" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <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/text[@Text='ViewSetup'] and
            enabled/text[@Text='true']
          ]
        ]]></value>
      </property>
      <property name="violationSuppressXPath" value="//Profile/custom/text[@Text='false']" />
    </properties>
  </rule>

  <rule name="ViewSetupPermissionSet" language="xml" message="ViewSetup must not be granted via a Permission Set" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>ViewSetup must not appear in Permission Sets.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //PermissionSet/userPermissions[
            name/text[@Text='ViewSetup'] and
            enabled/text[@Text='true']
          ]
        ]]></value>
      </property>
    </properties>
  </rule>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: SHARING MODEL GOVERNANCE                      -->
  <!-- ============================================================ -->

  <rule
        name="NonPrivateCustomObjectSharing"
        language="xml"
        message="Custom objects must use Private org-wide default sharing"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>Require Private OWD for all custom objects.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //CustomObject[
            (externalSharingModel | sharingModel)/text[@Text='Read' or @Text='ReadWrite'] and
            count(label) > 0
          ]
        ]]></value>
      </property>
    </properties>
  </rule>

  <rule
        name="NonPrivateStandardObjectSharing"
        language="xml"
        message="Standard objects must not use ReadWrite org-wide default sharing"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>Require non-ReadWrite OWD for standard objects.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //CustomObject[
            (externalSharingModel | sharingModel)/text[@Text='ReadWrite'] and
            count(label) = 0
          ]
        ]]></value>
      </property>
    </properties>
  </rule>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: METADATA DOCUMENTATION                        -->
  <!-- ============================================================ -->

  <rule name="MetadataDescriptionObjectField" language="xml" message="Add a description to explain this custom metadata" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>All custom objects and fields must have a description.</description>
    <priority>2</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //(CustomField | CustomObject | CustomObject/fields)/fullName[
            text and ../count(description) = 0
          ]
        ]]></value>
      </property>
    </properties>
  </rule>

  <rule
        name="MetadataDescriptionPermission"
        language="xml"
        message="Add a description to explain what access this metadata grants"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>All Permission Sets, Profiles, Custom Permissions, and Permission Set Groups must have a description.</description>
    <priority>2</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //(PermissionSet|Profile|CustomPermission|PermissionSetGroup)/label[
            text and
            ../count(description) = 0 and
            (
              ancestor::Profile/custom/text[@Text='true'] or
              count(ancestor::Profile/custom) = 0
            )
          ]
        ]]></value>
      </property>
    </properties>
  </rule>

  <rule name="MetadataDescriptionFlow" language="xml" message="Add a description to explain this Flow's purpose" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>All Flows must have a description.</description>
    <priority>2</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //Flow/label[text and ../count(description) = 0]
        ]]></value>
      </property>
    </properties>
  </rule>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: METADATA QUALITY                              -->
  <!-- ============================================================ -->

  <rule name="NoHardcodedWeblinkURLs" language="xml" message="Do not hardcode force.com domain URLs in weblinks" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>Weblink URLs must not contain hardcoded Salesforce instance URLs.</description>
    <priority>2</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //(CustomObject/webLinks/url/text)[matches(@Text, 'https://.*force.com')]
        ]]></value>
      </property>
    </properties>
  </rule>

  <!--
    Flags metadata files using API version below 58.
    The threshold of 58 represents a reasonable baseline - flag significantly
    outdated metadata without requiring perpetual updates to the latest version.
    Review and increment this threshold annually as the platform advances.
    Consider externalising this value to code-analyzer.yml to make threshold
    updates a configuration change rather than a ruleset XML change.
  -->
  <rule name="BumpApiVersion" language="xml" message="Metadata is using an outdated API version - update to a recent version" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>Metadata API versions below 58 should be updated.</description>
    <priority>3</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //apiVersion/text[number(@Image) < 58]
        ]]></value>
      </property>
    </properties>
  </rule>

  <!-- ============================================================ -->
  <!-- CUSTOM RULES: FLOW AND PROCESS BUILDER                      -->
  <!-- ============================================================ -->

  <rule
        name="DoNotUseProcessBuilder"
        language="xml"
        message="Process Builder is deprecated - migrate this automation to Flow"
        class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
    <description>All Process Builder automations must be migrated to Flow.</description>
    <priority>1</priority>
    <properties>
      <property name="xpath">
        <value><![CDATA[
          //Flow/processType/text[@Text='Workflow']
        ]]></value>
      </property>
    </properties>
  </rule>
</ruleset>

Priority Reference

PMD priorities in this ruleset map to Code Analyzer severity levels as follows:

PMD Priority Code Analyzer Severity Label Pipeline Behavior
1 1 Critical Blocking - fails the workflow
2 2 High Blocking - fails the workflow
3 3 Moderate Non-blocking - surfaced as advisory
4 4 Low Non-blocking - surfaced as advisory
5 5 Info Non-blocking - informational only

With --severity-threshold 2 configured in the GitHub Actions workflow, priorities 1 and 2 produce a non-zero exit code and block the PR. Priorities 3 through 5 are surfaced in the non-blocking violations table but do not prevent merging.