Automatically Execute, Report on, and Enforce your PMD Rules

In the last blog post we discussed how PMD works and gave some instructions on how to define and compile your own custom rules using Maven. That was a great exercise, but the act of explicitly invoking a custom PMD executable jar from the command line is rather clunky:

$JAVA_HOME/bin/java -jar target/pmd-rules-1.0.0-jar-with-dependencies.jar \
    -R category/apex/async-apex.xml \
    -d src/main/default/classes/Example.cls \
    -f json

How can we make the execution of these rules and the surfacing of their findings as effortless as possible? In this post we will share how to compile your custom PMD rules so that they can be registered with the SFDX Scanner CLI plugin, and how to automatically execute, report on, and strictly enforce your organization’s rules.

Project, POM, and Jar

Let’s take a look at our project’s directory structure:

.
├── LICENSE
├── README.md
├── config
│   └── project-scratch-def.json
├── force-app
│   └── main
│       └── default
│           └── classes
│               ├── Example.cls
│               └── Example.cls-meta.xml
├── pom.xml
├── sfdx-project.json
└── src
    └── main
        ├── java
        │   └── com
        │       └── mycompany
        │           └── pmd
        │               ├── AvoidFuture.java
        │               └── RequireFinalizer.java
        └── resources
            └── category
                └── apex
                    └── async-apex.xml

In this structure, you’ll observe two top level directories of interest; force-app which contains all of the Salesforce parts of our project, and src which contains the Java parts of our project. We are going to change the pom.xml up a little bit from the first post.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.mycompany.pmd</groupId>
  <artifactId>pmd-rules</artifactId>
  <version>1.0.0</version>

  <name>pmd-rules</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>net.sourceforge.pmd</groupId>
      <artifactId>pmd</artifactId>
      <version>6.55.0</version>
      <type>pom</type>
    </dependency>
    <dependency>
      <groupId>net.sourceforge.pmd</groupId>
      <artifactId>pmd-apex</artifactId>
      <version>6.55.0</version>
    </dependency>
    <dependency>
      <groupId>net.sourceforge.pmd</groupId>
      <artifactId>pmd-apex-jorje</artifactId>
      <version>6.55.0</version>
      <type>pom</type>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm</artifactId>
      <version>9.4</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <release>11</release>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <includes>
            <include>com/**/*.class</include>
            <include>category/apex/*.xml</include>
          </includes>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
            </manifest>
            <addMavenDescriptor>false</addMavenDescriptor>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

This pom acts slightly differently than before. Instead of compiling everything into one jar file with the maven-assembly-plugin, this uses the maven-jar-plugin to produce a scoped jar which just contains the compiled rule itself.

$ mvn package

Now, the project will have a file called target/pmd-rules-1.0.0.jar. This jar file is not executable on its own. If you inspect the contents, you will see that it only includes the compiled classes and XML files for the specific rules we have written:

$ unzip -l target/pmd-rules-1.0.0.jar


Archive:  target/pmd-rules-1.0.0.jar
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  05-29-2023 17:51   META-INF/
      697  05-29-2023 17:51   META-INF/MANIFEST.MF
        0  05-29-2023 17:51   category/
        0  05-29-2023 17:51   category/apex/
     1240  05-29-2023 17:51   category/apex/async-apex.xml
        0  05-29-2023 17:51   com/
        0  05-29-2023 17:51   com/mycompany/
        0  05-29-2023 17:51   com/mycompany/pmd/
     3263  05-29-2023 17:51   com/mycompany/pmd/RequireFinalizer.class
     1059  05-29-2023 17:51   com/mycompany/pmd/AvoidFuture.class
---------                     -------
     6259                     10 files

We could explicitly run our rules by calling the PMD executable and specifying the classPath to include this additional jar, but there is an easier way. The Salesforce Code Analyzer is a plugin for the SFDX CLI which contains PMD and ESLint and has built-in commands for registering custom rules.

# Install the SFDX CLI
$ npm install sfdx-cli -g

# Install the sfdx-scanner plugin
$ sfdx plugins:install @salesforce/sfdx-scanner

# Register the custom rules
$ sfdx scanner:rule:add -l apex -p target/pmd-rules-1.0.0.jar

# run the scan
$ sfdx scanner:run -t force-app/main/default/classes/Example.cls --json"

Findings

{
  "status": 0,
  "result": [
    {
      "engine": "pmd",
      "fileName": "/Users/mitchellspano/Documents/myProject/force-app/main/default/classes/Example.cls",
      "violations": [
        {
          "line": "6",
          "column": "3",
          "endLine": "6",
          "endColumn": "9",
          "severity": 3,
          "ruleName": "AvoidFuture",
          "category": "AsynchronousApex",
          "message": "Usage of the `@Future` annotation should be limited. Please consider implementing the `Queueable` interface instead."
        } // other findings also included
      ]
    }
  ],
  "warnings": [
    "We're continually improving Salesforce Code Analyzer. Tell us what you think! Give feedback at https://research.net/r/SalesforceCA"
  ]
}

Awesome! This allows us to run our PMD rules in addition to all of the base PMD rules which are already defined. However, we are still running this explicitly from the command line which is not ideal. What we want to do is have the system automatically execute these rules for us whenever a pull request is created or updated, and report the findings back to us in a way which makes sense.

SFDX Scan Pull Request GitHub Action

The SFDX Scan Pull Request GitHub action (written by yours truly) will execute the SFDX Scanner on the contents of a pull request and report the findings formatted as in-line comments or checks.

To use this action in your repository, just reference it in your project:

name: SFDX Scan Pull Request
on:
  pull_request:
    types: [opened, reopened, synchronize]
  workflow_dispatch:
jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Install SFDX CLI and Scanner
        run: |
          npm install sfdx-cli -g
          sfdx plugins:install @salesforce/sfdx-scanner

      - name: Run SFDX Scanner - Report findings as comments
        uses: mitchspano/sfdx-scan-pull-request@v0.1.14
        with:
          report-mode: comments
        env:
          GITHUB_TOKEN: $

The action has an optional input parameter called custom-pmd-rules which will enable you to register custom rules for use within the scan. Simply enter a JSON string to add any custom rules that need to be registered before the scan is executed. Custom rules are identified by the path to their XML/JAR file and their language.

- name: Run SFDX Scanner - Report findings as comments
  uses: mitchspano/sfdx-scan-pull-request@v0.1.14
  with:
    report-mode: comments
    custom-pmd-rules: '[{ "path": "target/pmd-rules-1.0.0.jar", "language": "apex" }]'
  env:
    GITHUB_TOKEN: $

Now, when the scan is executed, our custom rules will be included in the scan and any findings will be surfaced to the developers during their regular workflow:

This works well for reporting findings, but what if we want to enforce these rules?

Enforcement Mechanisms

There are two mechanisms within the scan that can help you to identify a violation as a warning or an error:

The first is called the severity-threshold. This is an input parameter which defines a threshold for the severity of the finding. Severity 1 is the highest priority and severity 5 is the lowest. If the threshold parameter is defined and the scan produces a finding at the threshold or more severe, then the action will fail.

The second is called strictly-enforced-rules. This is an input parameter which accepts a JSON string which defines the rules which will be strictly enforced regardless of their severity. Enforced rules are identified by their engine, category, and rule name.

[{ "engine": "pmd", "category": "Performance", "rule": "AvoidDebugStatements" }]

If an error is identified, the scan’s run will fail with the message ”One or more errors have been identified within the structure of the code that will need to be resolved before continuing”

Excellent! Now we have some really powerful capabilities. We can define our own rules, automatically execute a scan to detect violations of the rules, surface the findings in a readable way, and strictly enforce the rules we care about. Use these tools to automate a large portion of the code review process and enforce adherence to your organization’s Apex standards.