Enforce code standards with PMD

By

9 minutes de lecture

Developers working on a project usually set coding rules to have a standardized codebase. It is an important piece of the code maintainability, and it can be very easy to deviate from a rule or make mistakes. This is where tools like PMD come in play. PMD is a Java application that analyzes a code against a given set of rules. It can look at a wide variety of features, from naming conventions to cognitive complexity, or the risk of SOQL injection for example. It supports several languages, including Apex, so let’s see how we can use it to improve our code quality.

How to install and use PMD ? 

To install PMD, follow these instruction. As stated in the documentation, you will need Java JRE and Open JDK to run it. You could also create a permanent alias to run it quickly.

In order to run properly, PMD needs to know which rules to check when performing its analysis. This is done by giving it a file (called a ruleset) containing the list of rules to take in account. For the moment, let’s use the full standard set of Apex rules and discuss later how to customize it.

Once installed, we can run PMD using the following command:

pmd -d <path_to_file> -R <path_to_ruleset>

with the following parameters:

  • -d <path_to_file> : the path to the file or folder to be analyzed
  • -R <path_to_ruleset> : the path to the ruleset file
  • (optionnal) -f <format> : the output format among the available ones, such as JSON, HTML or colored text (default is plain text)

There are other options not used here but detailed in the proper documentation.

Analyze a file

Now we know how to run PMD, let’s see how it works on a piece of code. To illustrate how it works, we will use the following class, deliberately written in such a way that PMD will raise errors when analyzing it:

class testClass {
    public static void method1 () {
        String s = 'test';

        for (Integer i = 1; i < 100; i++) {
            List<Account> Accs = [select id, name from Account];
            if (Accs.size() > 0) system.Debug('Not empty list');
            for (account a : accs) {
                a.RecordTypeId = '012500000009WAr';
            }
        }
    }
}

We may notice some slight deviation with what is usually considered good code writing in the developer community. Let’s run PMD on this class with the previous command, and look at the output:

./testClass.cls:1:	ApexSharingViolations:	Apex classes should declare a sharing model if DML or SOQL/SOSL is used
./testClass.cls:1:	ClassNamingConventions:	The class name 'testClass' doesn't match '[A-Z][a-zA-Z0-9_]*'
./testClass.cls:2:	ApexDoc:	Missing ApexDoc comment
./testClass.cls:3:	UnusedLocalVariable:	Variable 's' defined but not used
./testClass.cls:6:	OperationWithLimitsInLoop:	Avoid operations in loops that may hit governor limits
./testClass.cls:6:	LocalVariableNamingConventions:	The local variable name 'Accs' doesn't match '[a-z][a-zA-Z0-9]*'
./testClass.cls:6:	ApexCRUDViolation:	Validate CRUD permission before SOQL/DML operation or enforce user mode
./testClass.cls:7:	AvoidDebugStatements:	Avoid debug statements since they impact on performance
./testClass.cls:7:	IfStmtsMustUseBraces:	Avoid using if statements without curly braces
./testClass.cls:7:	IfElseStmtsMustUseBraces:	Avoid using if...else statements without curly braces
./testClass.cls:7:	DebugsShouldUseLoggingLevel:	Calls to System.debug should specify a logging level.
./testClass.cls:9:	AvoidHardcodingId:	Hardcoding Ids is bound to break when changing environments.

As we can see, the output is a list of violations of the rules, with the line and a message about the violation. For instance, there is an issue with the class naming convention (ClassNamingConventions, line 1). PMD also detected the presence of SOQL in a loop (OperationWithLimitsInLoop, line 6). With that bit of information in mind, we can start correcting our code so that it fits the rules.

In some case, we might happen to have good reasons to willingly break some rule. Then we would want to tell PMD to ignore a class or a line. There are several ways to do so. First there is an annotation that can be added to a class to suppress PMD warnings. It works with a single rule, a list of rules or for all PMD entirely. This is done by placing one of the following lines before a class definition:

// This will suppress all the PMD warnings in this class
@SuppressWarnings('PMD')
// This will suppress a single rule warning in this class
@SuppressWarnings('PMD.ExcessiveClassLength')
// This will suppress a list of rule warnings in this class
@SuppressWarnings('PMD.EmptyCatchBlock, PMD.AvoidDebugStatements')

If we want to suppress rules at the line level, we can also use the //NOPMD comment, which tells PMD to ignore the line in its analysis. Finally, there are more complex ways to customize the warning suppression in the ruleset, based on rules properties. This method is defined in the documentation about suppressing warnings.

These feature allows us to bypass PMD for exceptional cases, but we need a more systematic approach of rule management for a full project. This is were the ruleset comes into play. It allows us to define the rules we want and those we don’t. Let’s see in the next part how to use it.

Building a ruleset

We mentioned earlier the ruleset file, where we configure the list of rules that should be checked by PMD. There are pre-defined rules, different for each supported language, that can be added to the ruleset. Let’s see how to setup and customize the ruleset.

Basic ruleset usage

First a word about the available rules. They are classified into categories, which are defined as follow for Apex:

  • Best practices: generic rules and best practices such those related to test classes or variable usage
  • Code Style: rules related to various naming conventions and clean code
  • Design: checks design metrics of the code, like complexity, class length or number of methods
  • Documentation: check the presence and concordance of ApexDoc for classes and methods (similar to JavaDoc)
  • Error Prone: check for code that could lead to bugs or errors, like empty block or hardcoded Ids
  • Performance: check code that could lead to performance issues, such as debugs statements or SOQL/DML in loops
  • Security: look for potential security issues such as SOQL injections

The ruleset is a XML file that contains all rules PMD should use. Here is its basic structure:

<?xml version="1.0" ?>
<ruleset
    name="My PMD ruleset"
    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>
        My PMD ruleset
    </description>
    <rule ref="category/apex/bestpractices.xml/UnusedLocalVariable">
    </rule>
</ruleset>

Here, the first lines contain the definition of the ruleset and its description. Then the <rule> tag adds a rule, with a reference containing its language (apex), category (bestpractices) and name (UnusedLocalVariable). This allows us to include a single rule, but we might want to add an entire category. This can be done with the same tag, by only including the name of the category:

<rule ref="category/apex/codestyle.xml"></rule>

When adding a category, we can choose to exclude a specific rule from that category. For instance, the following code adds all codestyle rules except the one called FieldDeclarationsShouldBeAtStart:

<rule ref="category/apex/codestyle.xml">
    <exclude name="FieldDeclarationsShouldBeAtStart" />
</rule>

Customizing the rules

Some rules have properties that can be set to configure their behavior. For instance, the AvoidDeeplyNestedIfStmts rule checks that there is no excessively nested if statements in the code. We can set the nesting threshold using the problemDepth parameter (default value is 3). There is also the option to customize the error message that get displayed.

<rule ref="category/apex/design.xml/AvoidDeeplyNestedIfStmts" message="There is more than 5 nested If statements">
    <properties>
        <property name="problemDepth" value="5" />
    </properties>
</rule>

With this feature, we can for instance customize rules with our own naming conventions. For rules dedicated to class and variable names, we can define regular expression to apply for different situations (such as test classes, static or final variables).

We also may need to ignore files from being processing by PMD with the <exclude-pattern> tag. And if a file should be included despite matching an exclude pattern, we can use the <include-pattern> to specify it. This comes in addition with the previously seen @SuppressWarnings and NOPMD features. For example, the following lines make PMD ignore all files ending by Test.cls but will still include the file SpecificTest.cls in the analysis:

<exclude-pattern>.*/force-app/main/default/classes/*Test.cls</exclude-pattern>
<include-pattern>.*/force-app/main/default/classes/SpecificTest.cls</include-pattern>

Until now, we only worked with the rules provided by PMD, but it may not be enough. If we need to customize even more our ruleset, there is the option to create our own rules. This can be done using XPath or by defining custom Java classes. It enables us to make complex calculations to evaluate one’s code. However, this feature is outside the article’s scope but there are some information about the way to handle it in the dedicated documentation. We can also check this example of ruleset for Apex, that defines custom rules with XPath relevant for Salesforce environments.

Summary and references

Coding standards are important in a team so that developers understand each other’s code easily. PMD can help us enforce them by pointing where and how a code can be improved. In this article, we saw how to implement it in a project using its main features, and to customize our rules so that it best fits our team’s standards. This software can be part of an CI/CD strategy, where the code is automatically analyzed before merging into the main branch. Note that there exists a tool that comes with PMD, which can detect duplicate code (CPD for Copy/Paste Detector). However, it doesn’t support Apex at the moment.

 

If you want to discover other articles, it is here !

Read more posts