As Salesforce developers, maintaining high-quality code is crucial for ensuring robust and efficient applications. One way to achieve this is by integrating static code analysis tools into your development process. In this blog post, we’ll explore how to set up a CI/CD pipeline to automate Salesforce code analysis using PMD, a popular static code analysis tool.
What is PMD?
PMD is an open-source static code analyzer that finds common programming flaws such as unused variables, empty catch blocks, unnecessary object creation, and more. By incorporating PMD into your Salesforce development workflow, you can catch potential issues early and improve the overall quality of your code.
Why Use a CI/CD Pipeline?
A Continuous Integration and Continuous Deployment (CI/CD) pipeline automates the process of integrating code changes, running tests, and deploying applications. By integrating PMD into a CI/CD pipeline, you ensure that code analysis is performed consistently and automatically with every code change. This helps in maintaining code quality and preventing issues from reaching production.
Setting Up the CI/CD Pipeline
We’ll use Bitbucket Pipelines for this example, but the concepts can be applied to other CI/CD tools like Jenkins, GitHub Actions, or GitLab CI.
Step 1: Define the Pipeline Configuration
Create a bitbucket-pipelines.yml file in the root of your Salesforce project repository. This file defines the pipeline configuration. Below is the configuration we will use:
image:
name: salesforce/salesforcedx:latest-full
pipelines:
pull-requests:
'**':
- step:
name: Salesforce Code Analyzer with PMD
caches:
- node
script:
- sfdx scanner:run --format html --target force-app --engine "pmd" --pmdconfig=config/pmd-rules.xml --outfile pmd-report.html
artifacts:
- pmd-report.html
Breakdown of the Configuration
- Image: Specifies the Docker image to use. In this case, we use
salesforce/salesforcedx:latest-full, which includes the Salesforce DX CLI and other necessary tools. - Pipelines: Defines the pipeline stages. Here, we configure the pipeline to run for all pull requests (
'**'). - Step: Describes the individual step in the pipeline:
- name: The name of the step, “Salesforce Code Analyzer with PMD”.
- caches: Specifies caching for dependencies to speed up the pipeline. We cache the
nodeenvironment. - script: Lists the commands to execute:
sfdx scanner:run --format html --target force-app --engine "pmd" --pmdconfig=config/pmd-rules.xml --outfile pmd-report.html: This command runs the PMD analysis using Salesforce’s scanner plugin. It targets theforce-appdirectory, uses PMD as the engine, specifies a custom PMD rules configuration file (config/pmd-rules.xml), and outputs the results topmd-report.html.
- artifacts: Specifies the files to be preserved after the pipeline runs. In this case, we keep
pmd-report.htmlso that we can review the analysis results.
Step 2: Configure PMD Rules
Create a PMD rules configuration file (pmd-rules.xml) in your project. This file defines the rules that PMD will use to analyze your code. Here’s a simple example:
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Apex Rules"
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
http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
<description>Custom PMD rules for Apex</description>
<!-- Best Practices -->
<rule ref="category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveRunAs" />
<rule ref="category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveAsserts" />
<rule ref="category/apex/bestpractices.xml/ApexUnitTestShouldNotUseSeeAllDataTrue" />
<rule ref="category/apex/bestpractices.xml/AvoidGlobalModifier" />
<rule ref="category/apex/bestpractices.xml/AvoidLogicInTrigger" />
<rule ref="category/apex/bestpractices.xml/UnusedLocalVariable" />
<!-- Code Styles -->
<rule ref="category/apex/codestyle.xml/ClassNamingConventions">
<properties>
<property name="testClassPattern" value="[A-Z][a-zA-Z0-9_]*Test" />
<property name="abstractClassPattern" value="[A-Z][a-zA-Z0-9_]*" />
<property name="classPattern" value="[A-Z][a-zA-Z0-9_]*" />
<property name="interfacePattern" value="[A-Z][a-zA-Z0-9_]*" />
<property name="enumPattern" value="[A-Z][a-zA-Z0-9_]*" />
</properties>
</rule>
<rule ref="category/apex/codestyle.xml/FieldNamingConventions">
<properties>
<property name="enumConstantPattern" value="[A-Z][A-Z0-9_]*" />
<property name="constantPattern" value="[A-Z][A-Z0-9_]*" />
<property name="finalPattern" value="[a-z][a-zA-Z0-9]*" />
<property name="staticPattern" value="[a-z][a-zA-Z0-9]*" />
<property name="instancePattern" value="[a-z][a-zA-Z0-9]*" />
</properties>
</rule>
<rule ref="category/apex/codestyle.xml/ForLoopsMustUseBraces" />
<rule ref="category/apex/codestyle.xml/IfElseStmtsMustUseBraces" />
<rule ref="category/apex/codestyle.xml/IfStmtsMustUseBraces" />
<rule ref="category/apex/codestyle.xml/LocalVariableNamingConventions" />
<rule ref="category/apex/codestyle.xml/MethodNamingConventions">
<properties>
<property name="testPattern" value="validate[A-Z][a-zA-Z0-9]*" />
<property name="staticPattern" value="[a-z][a-zA-Z0-9]*" />
<property name="instancePattern" value="[a-z][a-zA-Z0-9]*" />
</properties>
</rule>
<rule ref="category/apex/codestyle.xml/PropertyNamingConventions" />
<rule ref="category/apex/codestyle.xml/WhileLoopsMustUseBraces" />
<!-- Design -->
<rule ref="category/apex/design.xml/AvoidDeeplyNestedIfStmts">
<properties>
<property name="problemDepth" value="3" />
</properties>
</rule>
<rule ref="category/apex/design.xml/CognitiveComplexity">
<properties>
<property name="classReportLevel" value="50" />
<property name="methodReportLevel" value="15" />
</properties>
</rule>
<rule ref="category/apex/design.xml/CyclomaticComplexity">
<properties>
<property name="classReportLevel" value="40" />
<property name="methodReportLevel" value="10" />
</properties>
</rule>
<rule ref="category/apex/design.xml/ExcessiveClassLength">
<properties>
<property name="minimum" value="1000.0" />
</properties>
</rule>
<rule ref="category/apex/design.xml/ExcessiveParameterList">
<properties>
<property name="minimum" value="4.0" />
</properties>
</rule>
<rule ref="category/apex/design.xml/ExcessivePublicCount">
<properties>
<property name="minimum" value="20.0" />
</properties>
</rule>
<rule ref="category/apex/design.xml/StdCyclomaticComplexity">
<properties>
<property name="reportLevel" value="10" />
<property name="showClassesComplexity" value="true" />
<property name="showMethodsComplexity" value="true" />
</properties>
</rule>
<rule ref="category/apex/design.xml/TooManyFields">
<properties>
<property name="maxfields" value="15" />
</properties>
</rule>
<!-- Documentation -->
<rule ref="category/apex/documentation.xml/ApexDoc">
<properties>
<property name="reportPrivate" value="false" />
<property name="reportProtected" value="false" />
<property name="reportMissingDescription" value="true" />
<property name="reportProperty" value="true" />
</properties>
</rule>
<!-- Error Prone -->
<rule ref="category/apex/errorprone.xml/AvoidDirectAccessTriggerMap" />
<rule ref="category/apex/errorprone.xml/AvoidHardcodingId" />
<rule ref="category/apex/errorprone.xml/AvoidNonExistentAnnotations" />
<rule ref="category/apex/errorprone.xml/EmptyCatchBlock" />
<rule ref="category/apex/errorprone.xml/EmptyIfStmt" />
<rule ref="category/apex/errorprone.xml/EmptyStatementBlock" />
<rule ref="category/apex/errorprone.xml/EmptyTryOrFinallyBlock" />
<rule ref="category/apex/errorprone.xml/EmptyWhileStmt" />
<rule ref="category/apex/errorprone.xml/InaccessibleAuraEnabledGetter" />
<rule ref="category/apex/errorprone.xml/MethodWithSameNameAsEnclosingClass" />
<rule ref="category/apex/errorprone.xml/TestMethodsMustBeInTestClasses" />
<!-- Performance -->
<rule ref="category/apex/performance.xml/AvoidDebugStatements" />
<rule ref="category/apex/performance.xml/AvoidDmlStatementsInLoops" />
<rule ref="category/apex/performance.xml/AvoidSoqlInLoops" />
<rule ref="category/apex/performance.xml/AvoidSoslInLoops" />
<rule ref="category/apex/performance.xml/EagerlyLoadedDescribeSObjectResult" />
<rule ref="category/apex/performance.xml/OperationWithLimitsInLoop" />
<!-- Security -->
<rule ref="category/apex/security.xml/ApexBadCrypto" />
<rule ref="category/apex/security.xml/ApexCRUDViolation" />
<rule ref="category/apex/security.xml/ApexDangerousMethods" />
<rule ref="category/apex/security.xml/ApexInsecureEndpoint" />
<rule ref="category/apex/security.xml/ApexSharingViolations" />
<rule ref="category/apex/security.xml/ApexSOQLInjection" />
<rule ref="category/apex/security.xml/ApexSuggestUsingNamedCred" />
<rule ref="category/apex/security.xml/ApexXSSFromEscapeFalse" />
<rule ref="category/apex/security.xml/ApexXSSFromURLParam" />
<!-- VF Security -->
<rule ref="category/vf/security.xml/VfUnescapeEl" />
</ruleset>
you can add/remove rules as per your requirement.
Step 3: Commit and Push Your Changes
Once you’ve configured your pipeline and PMD rules, commit the bitbucket-pipelines.yml and pmd-rules.xml files to your repository:
git add bitbucket-pipelines.yml config/pmd-rules.xml
git commit -m "Add CI/CD pipeline configuration for PMD code analysis"
git push origin your-branch
Step 4: Review the Analysis Results
When you create a pull request, the pipeline will automatically run the PMD analysis. Once the pipeline completes, you can download and review the pmd-report.html file to see the analysis results.
Conclusion
Integrating PMD into your CI/CD pipeline is a powerful way to ensure consistent code quality in your Salesforce projects. By automating static code analysis, you can catch potential issues early and maintain a high standard of code across your development team. Give this setup a try and experience the benefits of automated code quality checks in your Salesforce development workflow.