One of the key requirements to developing good, maintainable software is to ensure that it works under a variety of conditions. This is typically done by automating a suite of tests on the various features and code paths your application can take. While unit tests are excellent for making sure that your application technically runs, there’s another category of verification that ensures that your application has no other detectable issues: static analysis.
Static analysis is a method of analyzing your code without executing it. If you’ve ever worked with a compiled language like Kotlin, the compiler implements one form of static analysis by ensuring that your program adheres to the grammatical rules of the language. For example, if you call a function but forget to pass in required arguments, a static analyzer alerts you to this error before you compile your application. This is in contrast to an interpreted language such as JavaScript, in which the error would occur when executing the code because there’s no compiler to anticipate the issue.
(To be technically precise, a static analyzer can be applied to interpreted languages like JavaScript, Ruby, or Python, ensuring that the code is well-formatted and has no missing logic.)
While a well-written test suite is likely to cover such code paths, static analysis can do so much more. A static analyzer can reduce the possibility of bugs, such as when you accidentally overwrite a variable with another value. It can also implement linting and formatting rules, which make your codebase consistent and easier to review. Some static analyzers even bring performance benefits by suggesting ways to rewrite loops or other functional calls.
Nearly every programming language has a static analyzer of its own. For example, golang has gofmt, which is baked into the standard tooling, while Ruby has Rubocop, a community-led project. Even compiled languages like C have their own static analyzer through astyle. However, it can be difficult (and tedious) to run several analyzers across polyglot projects. Fortunately, that’s where a project like PMD can be of assistance. PMD is a static analyzer that allows you to define a standard set of rules that can be applied over multiple languages.
In this post, we’ll take a closer look at PMD, and learn how to run it on Apex code. Our Apex project will have several issues that PMD can report and act on. We’ll also integrate PMD into your editor, as well as your CI environment, to make sure that your build will fail if the static analysis detects any problems.
Before getting started, you should have some familiarity with Apex, Salesforce’s programming language. We’ll be using VS Code along with the Apex plugin. You’ll also need the Salesforce CLI, which is a tool designed by Salesforce to simplify interacting with the platform.
Next, go ahead and follow the installation instructions for PMD.
Finally, clone our sample Apex project at this Git repository: https://github.com/gjtorikian/sfdc-linting.git
This repository is a forked copy of the dreamhouse-lwc project, except it has (intentionally) introduced some errors. We’ll use this to demonstrate how PMD works.
First, enable Dev Hub for your Salesforce organization. If you don’t have a Salesforce instance, no worries. You can create a scratch org, which is like a temporary Salesforce org. You can use the scratch org to test what developing on the Salesforce platform looks like.
Whether you’re using a scratch org or your own, you’ll need to associate sfdx with the org by logging in. Run the following command to do so:
sfdx auth:web:login
This will open a new browser window that will ask for your Salesforce credentials. When that’s finished, the Salesforce CLI will inform you when the authentication is complete.
Now, let’s see what happens when we try to upload the cloned project to our org. Navigate to the directory you cloned the dreamhouse-sfdx
project to, and run the following command:
sfdx force:source:push -u <admin_email_address>
You should see the following output:
*** Deploying with SOAP ***
Job ID | 0AfR000001XgjR1KAJ
SOURCE PROGRESS | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | 0/2 Components
TYPE PROJECT PATH PROBLEM
───── ──────────────────────────────────────────────── ──────────────────────────────────────────────
Error force-app/main/default/classes/PagedResult.cls Unexpected token '}'. (12:40)
Error force-app/main/default/classes/PagedResult.cls Unexpected token 'set'. (6:37)
ERROR running force:source:push: Push failed.
Uh oh! It looks like there were several issues in this file, stemming from missing semicolons. (How’d that get past code review?)
Open up force-app/main/default/classes/PagedResult.cls
in VS Code, and add a semicolon to the end of the statements on lines 6 and 12. That should solve the problem, right?
Well...perhaps not. While our code compiles and has no build errors, our project may have some other issues we’re not aware of. On the command line, type the following command:
pmd -d . -R config/ruleset.xml
Here, we’re running PMD on the current directory (-d .
), and we’re applying a ruleset located at config/ruleset.xml
. When you execute this command, you’ll see dozens of lines that look like this:
main/default/classes/PostPriceChangeToSlackTest.cls:11: DebugsShouldUseLoggingLevel: Calls to System.debug should specify a logging level.
main/default/classes/PropertyController.cls:1: AvoidGlobalModifier: Avoid using global modifier
main/default/classes/SampleDataController.cls:20: UnusedLocalVariable: Variable 'brokersJSON' defined but not used
This is the power of PMD. According to our ruleset, PMD identified several issues in our project:
Open up the config/ruleset.xml
file, and you’ll find an XML document that lists several rules
. These rules map to the issues which PMD will report on. Believe it or not, there are hundreds of Apex rules, and you can find the full set at the PMD repo. You have complete control over which rules to enable. Typically, you’d determine which ones are important by agreeing with your teammates on the ones that matter most. After all, their code will be statically analyzed, too!
Switching to the command line to statically analyze your code can get a bit tedious, if not outright disruptive to your workflow. Since static analysis looks at the structure of your code without compiling it, you can integrate tools like PMD directly into your editor. That means you can get feedback on your code as you’re writing it.
Fortunately, several plugins allow you to integrate PMD into VS Code. Let’s install one and see what that process looks like. Visit the Apex PMD extension homepage on the VS Code Marketplace and click Install. This downloads the plugin and installs it into your editor—but we’re not finished just yet.
The Apex PMD extension comes with its own ruleset which, while convenient, may not be the same rules you’ve established for your project. We’ll need to configure the tool to point to our predetermined ruleset.
Open up the VS Code settings page (this can be found in the menu bar under Code > Preferences > Settings), and type pmd
to filter the settings to just that extension. Then, in the Rulesets section, set the path to the ruleset.xml
file we created in this project.
Next, navigate to any .cls
file in your project. You’ll see varying squiggly lines indicating the issues found by PMD. Hovering over these lines also presents a dialog box indicating what the issue is, as well as which rule triggered it.
Having PMD run while writing your code is a good step towards catching issues before they enter production. However, the very best way to do this is to set up your PMD analysis as part of the test suite in your CI/CD pipeline.
The first step is to install PMD on your CI servers. You can do this by grabbing a containerized version of the program, or by downloading the package with wget and unzipping it.
If you’re using GitHub Actions, you can just integrate an action such as this one, which takes care of all the installation and configuration for you. If you’re not, you simply need to run PMD as you do on the CLI, within a script:
#!/bin/sh
set -e
pmd -d . -R config/ruleset.xml
Since PMD fails with a non-zero status, running this script will mark your CI as a failure, too, if there are any issues.
Static analysis not only helps keep your code consistent and clean, but it can also help make it more efficient by pointing out small inconsistencies that could add up to bigger problems. It should be an important tool in your toolbox. In addition, by integrating static analysis with your CI tests, you can rest assured that once your code is fixed, it stays fixed.
To learn more about how to write better Apex code, Salesforce has some Trailhead badges with some advanced topics. The PMD documentation also details all of the available Apex rules you can use. Salesforce also has a whole suite of tools that can be installed as plugins to make writing code much easier!
First published here