afv-library/skills/dx-code-analyzer-custom-rule-create/references/advanced-pmd-patterns.md

8.7 KiB

Advanced PMD Patterns

Multi-rule rulesets, overriding built-in rules, exclusion patterns, Java-based rules, and sharing rules across projects.

Multiple Rules in One Ruleset File

A single ruleset XML can contain any number of rules. This is the recommended approach for teams maintaining a shared rule set:

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="TeamStandards"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

    <description>Team coding standards</description>

    <!-- Rule 1: Custom XPath rule -->
    <rule name="NoSystemDebug" language="apex"
          message="Remove System.debug before merging"
          class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
        <priority>3</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
//MethodCallExpression[@FullMethodName='System.debug']
  [not(ancestor::UserClass[ModifierNode[@Test = true()]])]
]]></value>
            </property>
        </properties>
    </rule>

    <!-- Rule 2: Another custom XPath rule -->
    <rule name="SoqlInLoop" language="apex"
          message="SOQL query inside a loop — move query before the loop"
          class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
        <priority>2</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
//ForEachStatement//SoqlExpression | //ForLoopStatement//SoqlExpression | //WhileLoopStatement//SoqlExpression
]]></value>
            </property>
        </properties>
    </rule>

    <!-- Rule 3: XML metadata rule in the same file -->
    <rule name="MinApiVersion" language="xml"
          message="Update API version to 60.0+"
          class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
        <priority>3</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
//*[local-name()='apiVersion'][number(substring-before(text(), '.')) < 60]
]]></value>
            </property>
        </properties>
    </rule>
</ruleset>

Rules with different language values (apex, xml, visualforce, etc.) can coexist in one ruleset. PMD applies each rule only to files matching its language.

Overriding Built-in PMD Rules

Override severity, priority, or configurable properties on PMD's built-in rules without writing new ones:

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="CustomizedBuiltins"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

    <description>Customized built-in rule thresholds</description>

    <!-- Lower the complexity threshold from default 25 to 10 -->
    <rule ref="category/apex/design.xml/CyclomaticComplexity">
        <priority>2</priority>
        <properties>
            <property name="classReportLevel" value="40" />
            <property name="methodReportLevel" value="10" />
        </properties>
    </rule>

    <!-- Change excessive parameter count from default 4 to 3 -->
    <rule ref="category/apex/design.xml/ExcessiveParameterList">
        <properties>
            <property name="minimum" value="3" />
        </properties>
    </rule>

    <!-- Make debug statement rule high-severity instead of moderate -->
    <rule ref="category/apex/bestpractices.xml/DebugsShouldUseLoggingLevel">
        <priority>2</priority>
        <properties>
            <property name="strictMode" value="true" />
        </properties>
    </rule>

    <!-- Include an entire category with one exclusion -->
    <rule ref="category/apex/security.xml">
        <exclude name="ApexSuggestUsingNamedCred" />
    </rule>
</ruleset>

Configuration:

# code-analyzer.yml
engines:
  pmd:
    custom_rulesets:
      - "custom-rules/customized-builtins.xml"

How rule ref works

Pattern Effect
<rule ref="category/apex/design.xml/CyclomaticComplexity"> Override one specific rule
<rule ref="category/apex/security.xml"> Include entire category
<rule ref="..."><exclude name="RuleName"/></rule> Include category minus specific rules
Nested <priority> Override severity (1=Critical, 2=High, 3=Moderate, 4=Low, 5=Info)
Nested <properties> Override configurable thresholds/options

Common built-in rules to customize

Rule Useful Properties
CyclomaticComplexity classReportLevel (default: 40), methodReportLevel (default: 25)
ExcessiveParameterList minimum (default: 4)
ExcessiveClassLength minimum (default: 1000)
TooManyFields maxfields (default: 15)
NcssMethodCount minimum (default: 60)
DebugsShouldUseLoggingLevel strictMode (default: false)
ApexUnitTestClassShouldHaveAsserts (no properties — exclude if unwanted)

Exclusion Patterns

Three mechanisms for excluding files and rules:

1. File exclusions in code-analyzer.yml

Exclude entire directories or file patterns from all engines:

# code-analyzer.yml
ignores:
  files:
    - "**/node_modules/**"
    - "**/fflib_*"
    - "**/*Test*.cls"
    - "**/generated/**"

Supports glob patterns: *, **, ?, [...], {...}

2. Rule exclusions in PMD rulesets

Exclude specific rules when including an entire category:

<rule ref="category/apex/bestpractices.xml">
    <exclude name="ApexUnitTestClassShouldHaveAsserts" />
    <exclude name="ApexAssertionsShouldIncludeMessage" />
</rule>

3. Exclude patterns in PMD rulesets

Exclude files at the PMD level (applies only to rules in that ruleset):

<ruleset name="MyRules" ...>
    <exclude-pattern>.*/fflib_.*</exclude-pattern>
    <exclude-pattern>.*/test/.*</exclude-pattern>

    <rule name="..." ...>
        ...
    </rule>
</ruleset>

4. Disabling individual rules via config

Disable any rule without editing rulesets:

# code-analyzer.yml
rules:
  pmd:
    ApexUnitTestClassShouldHaveAsserts:
      disabled: true
    CyclomaticComplexity:
      severity: "Low"    # Downgrade instead of disable

Java-Based Custom Rules (Advanced)

For rules that need logic beyond XPath (complex data flow, cross-file analysis), write Java PMD rules:

  1. Create a Java class extending net.sourceforge.pmd.lang.apex.rule.AbstractApexRule
  2. Compile into a JAR file
  3. Reference in config:
# code-analyzer.yml
engines:
  pmd:
    java_classpath_entries:
      - "custom-rules/my-rules.jar"      # JAR with compiled rule classes
      - "custom-rules/lib/"              # Folder of JARs (all JARs inside loaded)
    custom_rulesets:
      - "category/myteam/rules.xml"      # Ruleset inside the JAR (classpath resource)

The JAR's ruleset XML references the Java class:

<rule name="AvoidFutureAnnotation" language="apex"
      message="Use Queueable instead of @Future"
      class="com.myteam.pmd.rules.AvoidFutureRule">
    <priority>3</priority>
</rule>

Requirements:

  • Java 11+ for compilation
  • PMD 7.x API (use net.sourceforge.pmd.lang.rule.xpath.XPathRule for XPath, or extend AbstractApexRule for visitor-pattern Java rules)

Sharing Rules Across Projects

Approach 1: Relative paths with shared config

If projects share a parent directory or monorepo:

# code-analyzer.yml
engines:
  pmd:
    custom_rulesets:
      - "../shared-rules/team-standards.xml"    # Relative to config_root

config_root is the directory containing code-analyzer.yml. All relative paths resolve from there.

Approach 2: JAR-based distribution

Package rules into a JAR and distribute via artifact repository:

  1. Build a JAR containing your ruleset XML at a classpath resource path (e.g., category/myteam/standards.xml)
  2. Place the JAR in each project's custom-rules/ directory (or download in CI)
  3. Reference in config:
engines:
  pmd:
    java_classpath_entries:
      - "custom-rules/myteam-rules-1.0.jar"
    custom_rulesets:
      - "category/myteam/standards.xml"    # Resource inside the JAR

For simpler setups, share the ruleset XML via git submodule:

git submodule add https://github.com/myteam/pmd-rules.git custom-rules/shared
engines:
  pmd:
    custom_rulesets:
      - "custom-rules/shared/team-standards.xml"

CI/CD Integration

All approaches work in CI because paths are relative to config_root. Example GitHub Actions step:

- name: Run Code Analyzer
  run: |
    sf code-analyzer run --rule-selector "pmd:2,regex:2" --target force-app/    

No absolute paths needed — the config file handles resolution.