From 5f106081afa755ded07ea228a0e9b74472d5dca7 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 3 Jul 2025 17:04:49 +0200 Subject: [PATCH 01/10] added PMD Apex rules - first try --- scripts/pmd7_rules_xml_generator.groovy | 22 +- sonar-pmd-plugin/pom.xml | 5 + .../org/sonar/plugins/pmd/PmdConstants.java | 3 + .../org/sonar/plugins/pmd/PmdExecutor.java | 6 + .../java/org/sonar/plugins/pmd/PmdPlugin.java | 2 + .../java/org/sonar/plugins/pmd/PmdSensor.java | 8 +- .../plugins/pmd/PmdViolationRecorder.java | 3 +- .../pmd/rule/PmdApexRulesDefinition.java | 72 + .../com/sonar/sqale/pmd-model-apex.xml | 14 + .../org/sonar/plugins/pmd/rules-apex.xml | 1884 +++++++++++++++++ .../org/sonar/plugins/pmd/PmdPluginTest.java | 4 +- .../org/sonar/plugins/pmd/PmdSensorTest.java | 4 +- 12 files changed, 2018 insertions(+), 9 deletions(-) create mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java create mode 100644 sonar-pmd-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml create mode 100644 sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml diff --git a/scripts/pmd7_rules_xml_generator.groovy b/scripts/pmd7_rules_xml_generator.groovy index b80bc912..1cb5293e 100644 --- a/scripts/pmd7_rules_xml_generator.groovy +++ b/scripts/pmd7_rules_xml_generator.groovy @@ -1,5 +1,6 @@ @Grab('net.sourceforge.pmd:pmd-java:7.15.0') @Grab('net.sourceforge.pmd:pmd-kotlin:7.15.0') +@Grab('net.sourceforge.pmd:pmd-apex:7.15.0') import groovy.xml.XmlSlurper import groovy.xml.MarkupBuilder import java.util.zip.ZipFile @@ -10,8 +11,10 @@ import java.util.regex.Matcher def pmdVersion = MdToHtmlConverter.PMD_VERSION def pmdJavaJarPath = "${System.getProperty("user.home")}/.m2/repository/net/sourceforge/pmd/pmd-java/${pmdVersion}/pmd-java-${pmdVersion}.jar" def pmdKotlinJarPath = "${System.getProperty("user.home")}/.m2/repository/net/sourceforge/pmd/pmd-kotlin/${pmdVersion}/pmd-kotlin-${pmdVersion}.jar" +def pmdApexJarPath = "${System.getProperty("user.home")}/.m2/repository/net/sourceforge/pmd/pmd-apex/${pmdVersion}/pmd-apex-${pmdVersion}.jar" def javaCategoriesPropertiesPath = "category/java/categories.properties" def kotlinCategoriesPropertiesPath = "category/kotlin/categories.properties" +def apexCategoriesPropertiesPath = "category/apex/categories.properties" // If we're in test mode, make the MdToHtmlConverter available but don't run the main code if (binding.hasVariable('TEST_MODE') && binding.getVariable('TEST_MODE')) { @@ -27,13 +30,16 @@ def defaultOutputDir = new File("sonar-pmd-plugin/src/main/resources/org/sonar/p def outputDirPath = binding.hasVariable('outputDir') ? outputDir : defaultOutputDir def javaOutputFileName = "rules-java.xml" def kotlinOutputFileName = "rules-kotlin.xml" +def apexOutputFileName = "rules-apex.xml" def javaOutputFilePath = new File(outputDirPath, javaOutputFileName) def kotlinOutputFilePath = new File(outputDirPath, kotlinOutputFileName) +def apexOutputFilePath = new File(outputDirPath, apexOutputFileName) println "PMD ${pmdVersion} Rules XML Generator" println "=" * 50 println "Java output file: ${javaOutputFilePath}" println "Kotlin output file: ${kotlinOutputFilePath}" +println "Apex output file: ${apexOutputFilePath}" /** * Groovy translation of MdToHtmlConverter @@ -1011,6 +1017,12 @@ def kotlinRules = readRulesFromJar(pmdKotlinJarPath, kotlinCategoriesPropertiesP println "Found ${kotlinRules.size()} total Kotlin rules" println "" +// Read Apex rules +println "Reading Apex rules from ${pmdApexJarPath}" +def apexRules = readRulesFromJar(pmdApexJarPath, apexCategoriesPropertiesPath) +println "Found ${apexRules.size()} total Apex rules" +println "" + // Helper function to convert priority to severity def priorityToSeverity = { priority -> switch (priority) { @@ -1226,9 +1238,15 @@ println "Generating Kotlin rules XML file..." println "=" * 30 def kotlinSuccess = generateXmlFile(kotlinOutputFilePath, kotlinRules, "Kotlin") +// Generate Apex rules XML file +println "" +println "Generating Apex rules XML file..." +println "=" * 30 +def apexSuccess = generateXmlFile(apexOutputFilePath, apexRules, "Apex") + println "" -if (javaSuccess && kotlinSuccess) { - println "XML generation completed successfully for both Java and Kotlin rules!" +if (javaSuccess && kotlinSuccess && apexSuccess) { + println "XML generation completed successfully for Java, Kotlin, and Apex rules!" } else { println "XML generation completed with errors. Please check the logs above." } diff --git a/sonar-pmd-plugin/pom.xml b/sonar-pmd-plugin/pom.xml index 49cbd329..678f30f5 100644 --- a/sonar-pmd-plugin/pom.xml +++ b/sonar-pmd-plugin/pom.xml @@ -122,6 +122,11 @@ pmd-kotlin ${pmd.version} + + net.sourceforge.pmd + pmd-apex + ${pmd.version} + org.jdom jdom2 diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java index d8bb5323..8b13294e 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java @@ -27,8 +27,10 @@ public final class PmdConstants { public static final String PLUGIN_KEY = "pmd"; public static final String MAIN_JAVA_REPOSITORY_KEY = PLUGIN_KEY; public static final String MAIN_KOTLIN_REPOSITORY_KEY = "pmd-kotlin"; + public static final String MAIN_APEX_REPOSITORY_KEY = "pmd-apex"; public static final String REPOSITORY_NAME = "PMD"; public static final String REPOSITORY_KOTLIN_NAME = "PMD Kotlin"; + public static final String REPOSITORY_APEX_NAME = "PMD Apex"; public static final String XPATH_CLASS = "net.sourceforge.pmd.lang.rule.xpath.XPathRule"; public static final String XPATH_EXPRESSION_PARAM = "xpath"; public static final String XPATH_MESSAGE_PARAM = "message"; @@ -57,6 +59,7 @@ public final class PmdConstants { */ public static final String LANGUAGE_JAVA_KEY = "java"; public static final String LANGUAGE_KOTLIN_KEY = "kotlin"; + public static final String LANGUAGE_APEX_KEY = "apex"; private PmdConstants() { } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java index ba6da5ce..24372be0 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java @@ -100,12 +100,16 @@ private Report executePmd(URLClassLoader classLoader) { final Optional javaTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_JAVA_KEY), PmdConstants.MAIN_JAVA_REPOSITORY_KEY); final Optional kotlinMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY); final Optional kotlinTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY); + final Optional apexMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); + final Optional apexTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); if (LOGGER.isDebugEnabled()) { javaMainReport.ifPresent(this::writeDebugLine); javaTestReport.ifPresent(this::writeDebugLine); kotlinMainReport.ifPresent(this::writeDebugLine); kotlinTestReport.ifPresent(this::writeDebugLine); + apexMainReport.ifPresent(this::writeDebugLine); + apexTestReport.ifPresent(this::writeDebugLine); } Consumer fileAnalysisListenerConsumer = PmdExecutor::accept; @@ -115,6 +119,8 @@ private Report executePmd(URLClassLoader classLoader) { unionReport = javaTestReport.map(unionReport::union).orElse(unionReport); unionReport = kotlinMainReport.map(unionReport::union).orElse(unionReport); unionReport = kotlinTestReport.map(unionReport::union).orElse(unionReport); + unionReport = apexMainReport.map(unionReport::union).orElse(unionReport); + unionReport = apexTestReport.map(unionReport::union).orElse(unionReport); pmdConfiguration.dumpXmlReport(unionReport); diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java index f8f45f06..1cce3a38 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java @@ -21,6 +21,7 @@ import org.sonar.api.Plugin; import org.sonar.api.config.PropertyDefinition; +import org.sonar.plugins.pmd.rule.PmdApexRulesDefinition; import org.sonar.plugins.pmd.rule.PmdKotlinRulesDefinition; import org.sonar.plugins.pmd.rule.PmdRulesDefinition; @@ -43,6 +44,7 @@ public void define(Context context) { PmdExecutor.class, PmdRulesDefinition.class, PmdKotlinRulesDefinition.class, + PmdApexRulesDefinition.class, PmdViolationRecorder.class ); } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java index 6de3ebb9..bda4e0f2 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java @@ -44,9 +44,11 @@ public PmdSensor(ActiveRules profile, PmdExecutor executor, PmdViolationRecorder private boolean shouldExecuteOnProject() { return (hasFilesToCheck(Type.MAIN, PmdConstants.MAIN_JAVA_REPOSITORY_KEY, PmdConstants.LANGUAGE_JAVA_KEY)) || (hasFilesToCheck(Type.TEST, PmdConstants.MAIN_JAVA_REPOSITORY_KEY, PmdConstants.LANGUAGE_JAVA_KEY)) - || (hasFilesToCheck(Type.MAIN, PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY) + || (hasFilesToCheck(Type.MAIN, PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY)) || (hasFilesToCheck(Type.TEST, PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY)) - ); + || (hasFilesToCheck(Type.MAIN, PmdConstants.MAIN_APEX_REPOSITORY_KEY, PmdConstants.LANGUAGE_APEX_KEY)) + || (hasFilesToCheck(Type.TEST, PmdConstants.MAIN_APEX_REPOSITORY_KEY, PmdConstants.LANGUAGE_APEX_KEY)) + ; } private boolean hasFilesToCheck(Type type, String repositoryKey, String languageKey) { @@ -64,7 +66,7 @@ public String toString() { @Override public void describe(SensorDescriptor descriptor) { - descriptor.onlyOnLanguages(PmdConstants.LANGUAGE_JAVA_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY) + descriptor.onlyOnLanguages(PmdConstants.LANGUAGE_JAVA_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY, PmdConstants.LANGUAGE_APEX_KEY) .name("PmdSensor"); } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java index d51c91d6..b4c01903 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java @@ -103,7 +103,8 @@ private RuleKey findActiveRuleKeyFor(RuleViolation violation) { return findRuleKey(internalRuleKey, PmdConstants.MAIN_JAVA_REPOSITORY_KEY) .orElse(findRuleKey(internalRuleKey, PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY) - .orElse(null)); + .orElse(findRuleKey(internalRuleKey, PmdConstants.MAIN_APEX_REPOSITORY_KEY) + .orElse(null))); } private Optional findRuleKey(String internalRuleKey, String repositoryKey) { diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java new file mode 100644 index 00000000..5dc56507 --- /dev/null +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java @@ -0,0 +1,72 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd.rule; + +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.plugins.pmd.PmdConstants; +import org.sonar.squidbridge.rules.SqaleXmlLoader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class PmdApexRulesDefinition implements RulesDefinition { + + private static final Logger LOGGER = Loggers.get(PmdApexRulesDefinition.class); + + public PmdApexRulesDefinition() { + // do nothing + } + + static void extractRulesData(NewRepository repository, String xmlRulesFilePath, String htmlDescriptionFolder) { + try (InputStream inputStream = PmdApexRulesDefinition.class.getResourceAsStream(xmlRulesFilePath)) { + if (inputStream == null) { + LOGGER.error("Cannot read {}", xmlRulesFilePath); + } + else { + new RulesDefinitionXmlLoader() + .load( + repository, + inputStream, + StandardCharsets.UTF_8 + ); + } + } catch (IOException e) { + LOGGER.error("Failed to load PMD RuleSet.", e); + } + + ExternalDescriptionLoader.loadHtmlDescriptions(repository, htmlDescriptionFolder); + SqaleXmlLoader.load(repository, "/com/sonar/sqale/pmd-model-apex.xml"); + } + + @Override + public void define(Context context) { + NewRepository repository = context + .createRepository(PmdConstants.MAIN_APEX_REPOSITORY_KEY, PmdConstants.LANGUAGE_APEX_KEY) + .setName(PmdConstants.REPOSITORY_APEX_NAME); + + extractRulesData(repository, "/org/sonar/plugins/pmd/rules-apex.xml", "/org/sonar/l10n/pmd/rules/pmd-apex"); + + repository.done(); + } + +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml b/sonar-pmd-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml new file mode 100644 index 00000000..cdfb5424 --- /dev/null +++ b/sonar-pmd-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml @@ -0,0 +1,14 @@ + + + REUSABILITY + Reusability + + MODULARITY + Modularity + + + TRANSPORTABILITY + Transportability + + + diff --git a/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml b/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml new file mode 100644 index 00000000..cc32ff19 --- /dev/null +++ b/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml @@ -0,0 +1,1884 @@ + + + + ApexAssertionsShouldIncludeMessage + Apex assertions should include message + category/apex/bestpractices.xml/ApexAssertionsShouldIncludeMessage + MAJOR + The second parameter of System.assert/third parameter of System.assertEquals/System.assertNotEquals is a message. +Having a second/third parameter provides more information and makes it easier to debug the test failure and +improves the readability of test output.

+

Example

+

 @isTest
+ public class Foo {
+     @isTest
+     static void methodATest() {
+         System.assertNotEquals('123', o.StageName); // not good
+         System.assertEquals('123', o.StageName, 'Opportunity stageName is wrong.'); // good
+         System.assert(o.isClosed); // not good
+         System.assert(o.isClosed, 'Opportunity is not closed.'); // good
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + ApexBadCrypto + Apex bad crypto + category/apex/security.xml/ApexBadCrypto + MAJOR + The rule makes sure you are using randomly generated IVs and keys for Crypto calls. +Hard-wiring these values greatly compromises the security of encrypted data.

+

Example

+

 public without sharing class Foo {
+     Blob hardCodedIV = Blob.valueOf('Hardcoded IV 123');
+     Blob hardCodedKey = Blob.valueOf('0000000000000000');
+     Blob data = Blob.valueOf('Data to be encrypted');
+     Blob encrypted = Crypto.encrypt('AES128', hardCodedKey, hardCodedIV, data);
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexCRUDViolation + Apex c r u d violation + category/apex/security.xml/ApexCRUDViolation + MAJOR + The rule validates you are checking for access permissions before a SOQL/SOSL/DML operation. +Since Apex runs by default in system mode not having proper permissions checks results in escalation of +privilege and may produce runtime errors. This check forces you to handle such scenarios.

+

Since Winter '23 (API Version 56) you can enforce user mode for database operations by using +WITH USER_MODE in SOQL. This makes Apex to respect Field-level security (FLS) and object +permissions of the running user. When using user mode, no violation is reported by this rule.

+

By default, the rule allows access checks can be performed using system Apex provisions such as +DescribeSObjectResult.isAccessible/Createable/etc., the SOQL WITH SECURITY_ENFORCED clause, +or using the open source Force.com ESAPI +class library. Because it is common to use authorization facades to assist with this task, the +rule also allows configuration of regular expression-based patterns for the methods used to +authorize each type of CRUD operation. These pattern are configured via the following properties:

+
  • createAuthMethodPattern/createAuthMethodTypeParamIndex - a pattern for the method used for create authorization and an optional 0-based index of the parameter passed to that method that denotes the SObjectType being authorized for create.
  • readAuthMethodPattern/readAuthMethodTypeParamIndex - a pattern for the method used for read authorization and an optional 0-based index of the parameter passed to that method that denotes the SObjectType being authorized for read.
  • updateAuthMethodPattern/updateAuthMethodTypeParamIndex - a pattern for the method used for update authorization and an optional 0-based index of the parameter passed to that method that denotes the SObjectType being authorized for update.
  • deleteAuthMethodPattern/deleteAuthMethodTypeParamIndex - a pattern for the method used for delete authorization and an optional 0-based index of the parameter passed to that method that denotes the SObjectType being authorized for delete.
  • undeleteAuthMethodPattern/undeleteAuthMethodTypeParamIndex - a pattern for the method used for undelete authorization and an optional 0-based index of the parameter passed to that method that denotes the SObjectType being authorized for undelete.
  • mergeAuthMethodPattern/mergeAuthMethodTypeParamIndex - a pattern for the method used for merge authorization and an optional 0-based index of the parameter passed to that method that denotes the SObjectType being authorized for merge.
+

The following example shows how the rule can be configured for the +sirono-common +AuthorizationUtil class:

+

 <rule ref="category/apex/security.xml/ApexCRUDViolation" message="Validate CRUD permission before SOQL/DML operation">
+     <priority>3</priority>
+     <properties>
+         <property name="createAuthMethodPattern" value="AuthorizationUtil\.(is|assert)(Createable|Upsertable)"/>
+         <property name="readAuthMethodPattern" value="AuthorizationUtil\.(is|assert)Accessible"/>
+         <property name="updateAuthMethodPattern" value="AuthorizationUtil\.(is|assert)(Updateable|Upsertable)"/>
+         <property name="deleteAuthMethodPattern" value="AuthorizationUtil\.(is|assert)Deletable"/>
+         <property name="undeleteAuthMethodPattern" value="AuthorizationUtil\.(is|assert)Undeletable"/>
+         <property name="mergeAuthMethodPattern" value="AuthorizationUtil\.(is|assert)Mergeable"/>
+     </properties>
+ </rule>

+

Note: This rule will produce false positives for VF getter methods. In VF getters the access permission +check happens automatically and is not needed explicitly. However, the rule can't reliably determine +whether a getter is a VF getter or not and reports a violation in any case. In such cases, the violation +should be suppressed.

+

Example

+

 public class Foo {
+     public Contact foo(String status, String ID) {
+ 
+         // validate you can actually query what you intend to retrieve
+         Contact c = [SELECT Status__c FROM Contact WHERE Id=:ID WITH SECURITY_ENFORCED];
+ 
+         // Make sure we can update the database before even trying
+         if (!Schema.sObjectType.Contact.fields.Status__c.isUpdateable()) {
+             return null;
+         }
+ 
+         c.Status__c = status;
+         update c;
+         return c;
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexCSRF + Apex c s r f + category/apex/errorprone.xml/ApexCSRF + MAJOR + Having DML operations in Apex class constructor or initializers can have unexpected side effects: + By just accessing a page, the DML statements would be executed and the database would be modified. + Just querying the database is permitted.

+

In addition to constructors and initializers, any method called init is checked as well.

+

Salesforce Apex already protects against this scenario and raises a runtime exception.

+

Note: This rule has been moved from category "Security" to "Error Prone" with PMD 6.21.0, since + using DML in constructors is not a security problem, but crashes the application.

+

Example

+

 public class Foo {
+     // initializer
+     {
+         insert data;
+     }
+ 
+     // static initializer
+     static {
+         insert data;
+     }
+ 
+     // constructor
+     public Foo() {
+         insert data;
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + ApexDangerousMethods + Apex dangerous methods + category/apex/security.xml/ApexDangerousMethods + MAJOR + Checks against calling dangerous methods.

+

For the time being, it reports:

+
  • Against FinancialForce's Configuration.disableTriggerCRUDSecurity(). Disabling CRUD security opens the door to several attacks and requires manual validation, which is unreliable.
  • Calling System.debug passing sensitive data as parameter, which could lead to exposure of private data.
+

Example

+

 public class Foo {
+     public Foo() {
+         Configuration.disableTriggerCRUDSecurity();
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexDoc + Apex doc + category/apex/documentation.xml/ApexDoc + MAJOR + This rule validates that:

+
  • ApexDoc comments are present for classes, methods, and properties that are public or global, excluding overrides and test classes (as well as the contents of test classes).
  • ApexDoc comments are present for classes, methods, and properties that are protected or private, depending on the properties reportPrivate and reportProtected.
  • ApexDoc comments should contain @description depending on the property reportMissingDescription.
  • ApexDoc comments on non-void, non-constructor methods should contain @return.
  • ApexDoc comments on void or constructor methods should not contain @return.
  • ApexDoc comments on methods with parameters should contain @param for each parameter, in the same order as the method signature.
  • ApexDoc comments are present on properties is only validated, if the property reportProperty is enabled. By setting reportProperty to false, you can ignore missing comments on properties.
+

Method overrides and tests are both exempted from having ApexDoc.

+

Example

+

 /**
+  * @description Hello World
+  */
+ public class HelloWorld {
+     /**
+      * @description Bar
+      * @return Bar
+      */
+     public Object bar() { return null; }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + documentation +
+ + ApexInsecureEndpoint + Apex insecure endpoint + category/apex/security.xml/ApexInsecureEndpoint + MAJOR + Checks against accessing endpoints under plain http. You should always use +https for security.

+

Example

+

 public without sharing class Foo {
+     void foo() {
+         HttpRequest req = new HttpRequest();
+         req.setEndpoint('http://localhost:com');
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexOpenRedirect + Apex open redirect + category/apex/security.xml/ApexOpenRedirect + MAJOR + Checks against redirects to user-controlled locations. This prevents attackers from +redirecting users to phishing sites.

+

Example

+

 public without sharing class Foo {
+     String unsafeLocation = ApexPage.getCurrentPage().getParameters.get('url_param');
+     PageReference page() {
+        return new PageReference(unsafeLocation);
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexSOQLInjection + Apex s o q l injection + category/apex/security.xml/ApexSOQLInjection + MAJOR + Detects the usage of untrusted / unescaped variables in DML queries.

+

Example

+

 public class Foo {
+     public void test1(String t1) {
+         Database.query('SELECT Id FROM Account' + t1);
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexSharingViolations + Apex sharing violations + category/apex/security.xml/ApexSharingViolations + MAJOR + Detect classes declared without explicit sharing mode if DML methods are used. This +forces the developer to take access restrictions into account before modifying objects.

+

Example

+

 public without sharing class Foo {
+     // DML operation here
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexSuggestUsingNamedCred + Apex suggest using named cred + category/apex/security.xml/ApexSuggestUsingNamedCred + MAJOR + Detects hardcoded credentials used in requests to an endpoint.

+

You should refrain from hardcoding credentials:

+
  • They are hard to mantain by being mixed in application code
  • Particularly hard to update them when used from different classes
  • Granting a developer access to the codebase means granting knowledge of credentials, keeping a two-level access is not possible.
  • Using different credentials for different environments is troublesome and error-prone.
+

Instead, you should use Named Credentials and a callout endpoint.

+

For more information, you can check this

+

Example

+

 public class Foo {
+     public void foo(String username, String password) {
+         Blob headerValue = Blob.valueOf(username + ':' + password);
+         String authorizationHeader = 'BASIC ' + EncodingUtil.base64Encode(headerValue);
+         req.setHeader('Authorization', authorizationHeader);
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexUnitTestClassShouldHaveAsserts + Apex unit test class should have asserts + category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveAsserts + MAJOR + Apex unit tests should include at least one assertion. This makes the tests more robust, and using assert +with messages provide the developer a clearer idea of what the test does. Custom assert method invocation +patterns can be specified using the 'additionalAssertMethodPattern' property if required.

+

Example

+

 @isTest
+ public class Foo {
+     public static testMethod void testSomething() {
+         Account a = null;
+         // This is better than having a NullPointerException
+         // System.assertNotEquals(a, null, 'account not found');
+         a.toString();
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + ApexUnitTestClassShouldHaveRunAs + Apex unit test class should have run as + category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveRunAs + MAJOR + Apex unit tests should include at least one runAs method. This makes the tests more robust, and independent from the +user running it.

+

Example

+

 @isTest
+ private class TestRunAs {
+    public static testMethod void testRunAs() {
+         // Setup test data
+         // Create a unique UserName
+         String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@testorg.com';
+         // This code runs as the system user
+         Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
+         User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
+         EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
+         LocaleSidKey='en_US', ProfileId = p.Id,
+         TimeZoneSidKey='America/Los_Angeles',
+          UserName=uniqueUserName);
+ 
+         System.runAs(u) {
+               // The following code runs as user 'u'
+               System.debug('Current User: ' + UserInfo.getUserName());
+               System.debug('Current Profile: ' + UserInfo.getProfileId());
+           }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + ApexUnitTestMethodShouldHaveIsTestAnnotation + Apex unit test method should have is test annotation + category/apex/bestpractices.xml/ApexUnitTestMethodShouldHaveIsTestAnnotation + MAJOR + Apex test methods should have @isTest annotation instead of the testMethod keyword, +as testMethod is deprecated. +Salesforce advices to use @isTest +annotation for test classes and methods.

+

Example

+

 @isTest
+ private class ATest {
+     @isTest
+     static void methodATest() {
+     }
+     static void methodBTest() {
+     }
+     @isTest static void methodCTest() {
+         System.assert(1==2);
+     }
+     static testmethod void methodCTest() {
+         System.debug('I am a debug statement');
+     }
+     private void fetchData() {
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + ApexUnitTestShouldNotUseSeeAllDataTrue + Apex unit test should not use see all data true + category/apex/bestpractices.xml/ApexUnitTestShouldNotUseSeeAllDataTrue + MAJOR + Apex unit tests should not use @isTest(seeAllData=true) because it opens up the existing database data for unexpected modification by tests.

+

Example

+

 @isTest(seeAllData = true)
+ public class Foo {
+     public static testMethod void testSomething() {
+         Account a = null;
+         // This is better than having a NullPointerException
+         // System.assertNotEquals(a, null, 'account not found');
+         a.toString();
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + ApexXSSFromEscapeFalse + Apex x s s from escape false + category/apex/security.xml/ApexXSSFromEscapeFalse + MAJOR + Reports on calls to addError with disabled escaping. The message passed to addError +will be displayed directly to the user in the UI, making it prime ground for XSS +attacks if unescaped.

+

Example

+

 public without sharing class Foo {
+     Trigger.new[0].addError(vulnerableHTMLGoesHere, false);
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + ApexXSSFromURLParam + Apex x s s from u r l param + category/apex/security.xml/ApexXSSFromURLParam + MAJOR + Makes sure that all values obtained from URL parameters are properly escaped / sanitized +to avoid XSS attacks.

+

Example

+

 public without sharing class Foo {
+     String unescapedstring = ApexPage.getCurrentPage().getParameters.get('url_param');
+     String usedLater = unescapedstring;
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + security +
+ + AvoidBooleanMethodParameters + Avoid boolean method parameters + category/apex/design.xml/AvoidBooleanMethodParameters + CRITICAL + Boolean parameters in a system's API can make method calls difficult to understand and + maintain. They often indicate that a method is doing more than one thing and + could benefit from being split into separate methods with more descriptive + names.

+

This rule flags any boolean parameters found in public or global methods, + encouraging developers to use more expressive alternatives such as enums, + separate methods, or configuration objects.

+

Examples

+

Example 1

+

 // Violates the rule: Uses a Boolean parameter
+ public class MyClass {
+   public static void doSomething(Boolean isSomething) {
+     if (isSomething == true) {
+       // Do something
+     } else {
+       // Do something else, or maybe do nothing if isSomething is null?
+     }
+   }
+ }
+ 
+ // Compliant code: Two separate methods
+ public class MyClass {
+   public static void doSomething() {
+     // Do something
+   }
+ 
+   public static void doSomethingElse() {
+     // Do something else
+   }
+ }

+

Example 2

+

 public void setFlag(Boolean strict) { ... } // violation
+ 
+ // compliant
+ public void enableStrictChecking() { ... }
+ public void disableStrictChecking() { ... }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + AvoidDebugStatements + Avoid debug statements + category/apex/performance.xml/AvoidDebugStatements + MAJOR + Debug statements contribute to longer transactions and consume Apex CPU time even when debug logs are not being captured.

+

When possible make use of other debugging techniques such as the Apex Replay Debugger and Checkpoints that could cover most use cases.

+

For other valid use cases that the statement is in fact valid make use of the @SuppressWarnings annotation or the //NOPMD comment.

+

Example

+

 public class Foo {
+     public void bar() {
+         Account acc = [SELECT Name, Owner.Name FROM Account LIMIT 1];
+         System.debug(accs); // will get reported
+     }
+ 
+     @SuppressWarnings('PMD.AvoidDebugStatements')
+     public void baz() {
+         try {
+             Account myAccount = bar();
+         } catch (Exception e) {
+             System.debug(LoggingLevel.ERROR, e.getMessage()); // good to go
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + performance +
+ + AvoidDeeplyNestedIfStmts + Avoid deeply nested if stmts + category/apex/design.xml/AvoidDeeplyNestedIfStmts + MAJOR + Avoid creating deeply nested if-then statements since they are harder to read and error-prone to maintain.

+

Example

+

 public class Foo {
+     public void bar(Integer x, Integer y, Integer z) {
+         if (x>y) {
+             if (y>z) {
+                 if (z==x) {
+                     // !! too deep
+                 }
+             }
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + AvoidDirectAccessTriggerMap + Avoid direct access trigger map + category/apex/errorprone.xml/AvoidDirectAccessTriggerMap + MAJOR + Avoid directly accessing Trigger.old and Trigger.new as it can lead to a bug. Triggers should be bulkified and iterate through the map to handle the actions for each item separately.

+

Example

+

 trigger AccountTrigger on Account (before insert, before update) {
+    Account a = Trigger.new[0]; //Bad: Accessing the trigger array directly is not recommended.
+ 
+    for ( Account a : Trigger.new ) {
+         //Good: Iterate through the trigger.new array instead.
+    }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + AvoidGlobalModifier + Avoid global modifier + category/apex/bestpractices.xml/AvoidGlobalModifier + MAJOR + Global classes should be avoided (especially in managed packages) as they can never be deleted or changed in signature. Always check twice if something needs to be global. +Many interfaces (e.g. Batch) required global modifiers in the past but don't require this anymore. Don't lock yourself in.

+

Example

+

 global class Unchangeable {
+     global UndeletableType unchangable(UndeletableType param) {
+         // ...
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + AvoidHardcodingId + Avoid hardcoding id + category/apex/errorprone.xml/AvoidHardcodingId + MAJOR + When deploying Apex code between sandbox and production environments, or installing Force.com AppExchange packages, + it is essential to avoid hardcoding IDs in the Apex code. By doing so, if the record IDs change between environments, + the logic can dynamically identify the proper data to operate against and not fail.

+

Example

+

 public without sharing class Foo {
+     void foo() {
+         //Error - hardcoded the record type id
+         if (a.RecordTypeId == '012500000009WAr') {
+             //do some logic here.....
+         } else if (a.RecordTypeId == '0123000000095Km') {
+             //do some logic here for a different record type...
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + AvoidLogicInTrigger + Avoid logic in trigger + category/apex/bestpractices.xml/AvoidLogicInTrigger + MAJOR + As triggers do not allow methods like regular classes they are less flexible and suited to apply good encapsulation style. +Therefore delegate the triggers work to a regular class (often called Trigger handler class).

+

See more here: https://developer.salesforce.com/page/Trigger_Frameworks_and_Apex_Trigger_Best_Practices

+

Example

+

 trigger Accounts on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
+     for(Account acc : Trigger.new) {
+         if(Trigger.isInsert) {
+             // ...
+         }
+ 
+         // ...
+ 
+         if(Trigger.isDelete) {
+             // ...
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + AvoidNonExistentAnnotations + Avoid non existent annotations + category/apex/errorprone.xml/AvoidNonExistentAnnotations + MAJOR + Apex supported non existent annotations for legacy reasons. + In the future, use of such non-existent annotations could result in broken apex code that will not compile. + This will prevent users of garbage annotations from being able to use legitimate annotations added to Apex in the future. + A full list of supported annotations can be found at https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation.htm

+

Example

+

 @NonExistentAnnotation public class ClassWithNonexistentAnnotation {
+     @NonExistentAnnotation public void methodWithNonExistentAnnotation() {
+         // ...
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + AvoidNonRestrictiveQueries + Avoid non restrictive queries + category/apex/performance.xml/AvoidNonRestrictiveQueries + MAJOR + When working with very large amounts of data, unfiltered SOQL or SOSL queries can quickly cause + governor limit + exceptions.

+

Example

+

 public class Something {
+     public static void main( String[] as ) {
+         Account[] accs1 = [ select id from account ];  // Bad
+         Account[] accs2 = [ select id from account limit 10 ];  // better
+ 
+         List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead]; // bad
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + performance +
+ + AvoidStatefulDatabaseResult + Avoid stateful database result + category/apex/errorprone.xml/AvoidStatefulDatabaseResult + CRITICAL + Using instance variables of the following types (or collections of these types) within a stateful batch class can cause serialization errors between batch iterations:

+
  • Database.DeleteResult
  • Database.EmptyRecycleBinResult
  • Database.MergeResult
  • Database.SaveResult
  • Database.UndeleteResult
  • Database.UpsertResult
+

This error occurs inconsistently and asynchronously with an obscure error message - making it particularly challenging to troubleshoot. + See this issue for more details.

+

These errors can be avoided by marking the variable as static, transient, or using a different + data type that is safe to serialize.

+

Example

+

 // Violating
+ public class Example implements Database.Batchable<SObject>, Database.Stateful {
+   List<Database.SaveResult> results = new List<Database.SaveResult>(); // This can cause failures
+ 
+   public Database.Querylocator start(Database.BatchableContext context) {
+     return Database.getQueryLocator('SELECT Id FROM Account');
+   }
+ 
+   public void execute(Database.BatchableContext context, List<SObject> scope) {
+     Database.SaveResult[] saveResults = Database.update(scope, false);
+     results.addAll(saveResults);
+   }
+ 
+   public void finish(database.BatchableContext context) {
+   }
+ }
+ 
+ // Compliant
+ public class Example implements Database.Batchable<SObject>, Database.Stateful {
+   List<StatefulResult> results = new List<StatefulResult>(); // Use a different custom type to persist state
+ 
+   public Database.Querylocator start(Database.BatchableContext context) {
+     return Database.getQueryLocator('SELECT Id FROM Account');
+   }
+ 
+   public void execute(Database.BatchableContext context, List<SObject> scope) {
+     Database.SaveResult[] saveResults = Database.update(scope, false);
+     for (Database.SaveResult result : saveResults) {
+       results.add(new StatefulResult(result));
+     }
+   }
+ 
+   public void finish(database.BatchableContext context) {
+   }
+ 
+ }
+ 
+ public class StatefulResult {
+   private Boolean isSuccess;
+   private Id id;
+   private Database.Error[] errors;
+ 
+   public StatefulResult(Database.SaveResult result) {
+     isSuccess = result.isSuccess();
+     id = result.getId();
+     errors = result.getErrors();
+   }
+ 
+   public Boolean isSuccess() {
+     return isSuccess;
+   }
+ 
+   public Id getId() {
+     return id;
+   }
+ 
+   public Database.Error[] getErrors() {
+     return errors;
+   }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + ClassNamingConventions + Class naming conventions + category/apex/codestyle.xml/ClassNamingConventions + BLOCKER + Configurable naming conventions for type declarations. This rule reports + type declarations which do not match the regex that applies to their + specific kind (e.g. enum or interface). Each regex can be configured through + properties.

+

By default this rule uses the standard Apex naming convention (Pascal case).

+

Example

+

 public class FooClass { } // This is in pascal case, so it's ok
+ 
+ public class fooClass { } // This will be reported unless you change the regex

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + CognitiveComplexity + Cognitive complexity + category/apex/design.xml/CognitiveComplexity + MAJOR + Methods that are highly complex are difficult to read and more costly to maintain. If you include too much decisional +logic within a single method, you make its behavior hard to understand and more difficult to modify.

+

Cognitive complexity is a measure of how difficult it is for humans to read and understand a method. Code that contains +a break in the control flow is more complex, whereas the use of language shorthands doesn't increase the level of +complexity. Nested control flows can make a method more difficult to understand, with each additional nesting of the +control flow leading to an increase in cognitive complexity.

+

Information about Cognitive complexity can be found in the original paper here: +https://www.sonarsource.com/docs/CognitiveComplexity.pdf

+

By default, this rule reports methods with a complexity of 15 or more. Reported methods should be broken down into less +complex components.

+

Example

+

 public class Foo {
+     // Has a cognitive complexity of 0
+     public void createAccount() {
+         Account account = new Account(Name = 'PMD');
+         insert account;
+     }
+ 
+     // Has a cognitive complexity of 1
+     public Boolean setPhoneNumberIfNotExisting(Account a, String phone) {
+         if (a.Phone == null) {                          // +1
+             a.Phone = phone;
+             update a;
+             return true;
+         }
+ 
+         return false;
+     }
+ 
+     // Has a cognitive complexity of 4
+     public void updateContacts(List<Contact> contacts) {
+         List<Contact> contactsToUpdate = new List<Contact>();
+ 
+         for (Contact contact : contacts) {                           // +1
+             if (contact.Department == 'Finance') {                   // +2 (nesting = 1)
+                 contact.Title = 'Finance Specialist';
+                 contactsToUpdate.add(contact);
+             } else if (contact.Department == 'Sales') {              // +1
+                 contact.Title = 'Sales Specialist';
+                 contactsToUpdate.add(contact);
+             }
+         }
+ 
+         update contactsToUpdate;
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + CyclomaticComplexity + Cyclomatic complexity + category/apex/design.xml/CyclomaticComplexity + MAJOR + The complexity of methods directly affects maintenance costs and readability. Concentrating too much decisional logic +in a single method makes its behaviour hard to read and change.

+

Cyclomatic complexity assesses the complexity of a method by counting the number of decision points in a method, +plus one for the method entry. Decision points are places where the control flow jumps to another place in the +program. As such, they include all control flow statements, such as 'if', 'while', 'for', and 'case'.

+

Generally, numbers ranging from 1-4 denote low complexity, 5-7 denote moderate complexity, 8-10 denote +high complexity, and 11+ is very high complexity. By default, this rule reports methods with a complexity >= 10. +Additionally, classes with many methods of moderate complexity get reported as well once the total of their +methods' complexities reaches 40, even if none of the methods was directly reported.

+

Reported methods should be broken down into several smaller methods. Reported classes should probably be broken down +into subcomponents.

+

Example

+

 public class Complicated {
+   public void example() { // This method has a cyclomatic complexity of 12
+     int x = 0, y = 1, z = 2, t = 2;
+     boolean a = false, b = true, c = false, d = true;
+     if (a && b || b && d) {
+       if (y == z) {
+         x = 2;
+       } else if (y == t && !d) {
+         x = 2;
+       } else {
+         x = 2;
+       }
+     } else if (c && d) {
+       while (z < y) {
+         x = 2;
+       }
+     } else {
+       for (int n = 0; n < t; n++) {
+         x = 2;
+       }
+     }
+   }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + DebugsShouldUseLoggingLevel + Debugs should use logging level + category/apex/bestpractices.xml/DebugsShouldUseLoggingLevel + MAJOR + The first parameter of System.debug, when using the signature with two parameters, is a LoggingLevel enum.

+

Having the Logging Level specified provides a cleaner log, and improves readability of it.

+

Example

+

 @isTest
+ public class Foo {
+     @isTest
+     static void bar() {
+         System.debug('Hey this code executed.'); // not good
+         System.debug(LoggingLevel.WARN, 'Hey, something might be wrong.'); // good
+         System.debug(LoggingLevel.DEBUG, 'Hey, something happened.'); // not good when on strict mode
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices + + strictMode + + false + BOOLEAN + +
+ + EagerlyLoadedDescribeSObjectResult + Eagerly loaded describe s object result + category/apex/performance.xml/EagerlyLoadedDescribeSObjectResult + MAJOR + This rule finds DescribeSObjectResults which could have been loaded eagerly via SObjectType.getDescribe().

+

When using SObjectType.getDescribe() or Schema.describeSObjects() without supplying a SObjectDescribeOptions, +implicitly it will be using SObjectDescribeOptions.DEFAULT and then all +child relationships will be loaded eagerly regardless whether this information is needed or not. +This has a potential negative performance impact. Instead SObjectType.getDescribe(options) +or Schema.describeSObjects(SObjectTypes, options) +should be used and a SObjectDescribeOptions should be supplied. By using +SObjectDescribeOptions.DEFERRED the describe attributes will be lazily initialized at first use.

+

Lazy loading DescribeSObjectResult on picklist fields is not always recommended. The lazy loaded +describe objects might not be 100% accurate. It might be safer to explicitly use +SObjectDescribeOptions.FULL in such a case. The same applies when you need the same DescribeSObjectResult +to be consistent across different contexts and API versions.

+

Properties:

+
  • noDefault: The behavior of SObjectDescribeOptions.DEFAULT changes from API Version 43 to 44: With API Version 43, the attributes are loaded eagerly. With API Version 44, they are loaded lazily. Simply using SObjectDescribeOptions.DEFAULT doesn't automatically make use of lazy loading. (unless "Use Improved Schema Caching" critical update is applied, SObjectDescribeOptions.DEFAULT does fallback to lazy loading) With this property enabled, such usages are found. You might ignore this, if you can make sure, that you don't run a mix of API Versions.
+

Example

+

 public class Foo {
+     public static void bar(List<Account> accounts) {
+         if (Account.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).isCreateable()) {
+             insert accounts;
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + performance + + noDefault + + false + BOOLEAN + +
+ + EmptyCatchBlock + Empty catch block + category/apex/errorprone.xml/EmptyCatchBlock + MAJOR + Empty Catch Block finds instances where an exception is caught, but nothing is done. + In most circumstances, this swallows an exception which should either be acted on + or reported.

+

Example

+

 public void doSomething() {
+     ...
+     try {
+         insert accounts;
+     } catch (DmlException dmle) {
+         // not good
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone + + allowCommentedBlocks + + false + BOOLEAN + + + allowExceptionNameRegex + + ^(ignored|expected)$ + STRING + +
+ + EmptyIfStmt + Empty if stmt + category/apex/errorprone.xml/EmptyIfStmt + MAJOR + Empty If Statement finds instances where a condition is checked but nothing is done about it.

+

Example

+

 public class Foo {
+     public void bar(Integer x) {
+         if (x == 0) {
+             // empty!
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + EmptyStatementBlock + Empty statement block + category/apex/errorprone.xml/EmptyStatementBlock + MAJOR + Empty block statements serve no purpose and should be removed.

+

Example

+

 public class Foo {
+ 
+    private Integer _bar;
+ 
+    public void setBar(Integer bar) {
+         // empty
+    }
+ 
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone + + reportEmptyPrivateNoArgConstructor + + true + BOOLEAN + + + reportEmptyVirtualMethod + + true + BOOLEAN + +
+ + EmptyTryOrFinallyBlock + Empty try or finally block + category/apex/errorprone.xml/EmptyTryOrFinallyBlock + MAJOR + Avoid empty try or finally blocks - what's the point?

+

Example

+

 public class Foo {
+     public void bar() {
+         try {
+           // empty !
+         } catch (Exception e) {
+             e.printStackTrace();
+         }
+     }
+ }
+ 
+ public class Foo {
+     public void bar() {
+         try {
+             Integer x=2;
+         } finally {
+             // empty!
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + EmptyWhileStmt + Empty while stmt + category/apex/errorprone.xml/EmptyWhileStmt + MAJOR + Empty While Statement finds all instances where a while statement does nothing. + If it is a timing loop, then you should use Thread.sleep() for it; if it is + a while loop that does a lot in the exit expression, rewrite it to make it clearer.

+

Example

+

 public void bar(Integer a, Integer b) {
+   while (a == b) {
+     // empty!
+   }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + ExcessiveClassLength + Excessive class length + category/apex/design.xml/ExcessiveClassLength + MAJOR + Excessive class file lengths are usually indications that the class may be burdened with excessive +responsibilities that could be provided by external classes or functions. In breaking these methods +apart the code becomes more managable and ripe for reuse.

+

Example

+

 public class Foo {
+     public void bar1() {
+         // 1000 lines of code
+     }
+     public void bar2() {
+         // 1000 lines of code
+     }
+     public void bar3() {
+         // 1000 lines of code
+     }
+     public void barN() {
+         // 1000 lines of code
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + ExcessiveParameterList + Excessive parameter list + category/apex/design.xml/ExcessiveParameterList + MAJOR + Methods with numerous parameters are a challenge to maintain, especially if most of them share the +same datatype. These situations usually denote the need for new objects to wrap the numerous parameters.

+

Example

+

 // too many arguments liable to be mixed up
+ public void addPerson(Integer birthYear, Integer birthMonth, Integer birthDate, Integer height, Integer weight, Integer ssn) {
+     // ...
+ }
+ // preferred approach
+ public void addPerson(Date birthdate, BodyMeasurements measurements, int ssn) {
+     // ...
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + ExcessivePublicCount + Excessive public count + category/apex/design.xml/ExcessivePublicCount + MAJOR + Classes with large numbers of public methods, attributes, and properties require disproportionate testing efforts +since combinatorial side effects grow rapidly and increase risk. Refactoring these classes into +smaller ones not only increases testability and reliability but also allows new variations to be +developed easily.

+

Example

+

 public class Foo {
+     public String value;
+     public Bar something;
+     public Variable var;
+     // [... more more public attributes ...]
+ 
+     public void doWork() {}
+     public void doMoreWork() {}
+     public void doWorkAgain() {}
+     // [... more more public methods ...]
+ 
+     public String property1 { get; set; }
+     // [... more more public properties ...]
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + FieldDeclarationsShouldBeAtStart + Field declarations should be at start + category/apex/codestyle.xml/FieldDeclarationsShouldBeAtStart + MAJOR + Field declarations should appear before method declarations within a class.

+

Example

+

 class Foo {
+     public Integer someField; // good
+ 
+     public void someMethod() {
+     }
+ 
+     public Integer anotherField; // bad
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + FieldNamingConventions + Field naming conventions + category/apex/codestyle.xml/FieldNamingConventions + BLOCKER + Configurable naming conventions for field declarations. This rule reports variable declarations + which do not match the regex that applies to their specific kind ---e.g. constants (static final), + static field, final field. Each regex can be configured through properties.

+

By default this rule uses the standard Apex naming convention (Camel case).

+

Example

+

 public class Foo {
+     Integer instanceField; // This is in camel case, so it's ok
+ 
+     Integer INSTANCE_FIELD; // This will be reported unless you change the regex
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + ForLoopsMustUseBraces + For loops must use braces + category/apex/codestyle.xml/ForLoopsMustUseBraces + MAJOR + Avoid using 'for' statements without using surrounding braces. If the code formatting or +indentation is lost then it becomes difficult to separate the code being controlled +from the rest.

+

Example

+

 for (int i = 0; i < 42; i++) // not recommended
+     foo();
+ 
+ for (int i = 0; i < 42; i++) { // preferred approach
+     foo();
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + FormalParameterNamingConventions + Formal parameter naming conventions + category/apex/codestyle.xml/FormalParameterNamingConventions + BLOCKER + Configurable naming conventions for formal parameters of methods. + This rule reports formal parameters which do not match the regex that applies to their + specific kind (e.g. method parameter, or final method parameter). Each regex can be + configured through properties.

+

By default this rule uses the standard Apex naming convention (Camel case).

+

Example

+

 public class Foo {
+     public bar(Integer methodParameter) { } // This is in camel case, so it's ok
+ 
+     public baz(Integer METHOD_PARAMETER) { } // This will be reported unless you change the regex
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + IfElseStmtsMustUseBraces + If else stmts must use braces + category/apex/codestyle.xml/IfElseStmtsMustUseBraces + MAJOR + Avoid using if..else statements without using surrounding braces. If the code formatting +or indentation is lost then it becomes difficult to separate the code being controlled +from the rest.

+

Example

+

 // this is OK
+ if (foo) x++;
+ 
+ // but this is not
+ if (foo)
+     x = x+1;
+ else
+     x = x-1;

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + IfStmtsMustUseBraces + If stmts must use braces + category/apex/codestyle.xml/IfStmtsMustUseBraces + MAJOR + Avoid using if statements without using braces to surround the code block. If the code +formatting or indentation is lost then it becomes difficult to separate the code being +controlled from the rest.

+

Example

+

 if (foo)    // not recommended
+     x++;
+ 
+ if (foo) {  // preferred approach
+     x++;
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + InaccessibleAuraEnabledGetter + Inaccessible aura enabled getter + category/apex/errorprone.xml/InaccessibleAuraEnabledGetter + MAJOR + In the Summer '21 release, a mandatory security update enforces access modifiers on Apex properties in + Lightning component markup. The update prevents access to private or protected Apex getters from Aura + and Lightning Web Components.

+

Examples

+

Example 1

+

 public class Foo {
+     @AuraEnabled
+     public Integer counter { private get; set; } // Violating - Private getter is inaccessible to Lightning components
+ 
+     @AuraEnabled
+     public static Foo bar()
+     {
+         Foo foo = new Foo();
+         foo.counter = 2; 
+         return foo;
+     }
+ }

+

Example 2

+

 public class Foo {
+     @AuraEnabled
+     public Integer counter { protected get; set; } // Violating - Protected getter is inaccessible to Lightning components
+ 
+     @AuraEnabled
+     public static Foo bar()
+     {
+         Foo foo = new Foo();
+         foo.counter = 2; 
+         return foo;
+     }
+ }

+

Example 3

+

 public class Foo {
+     @AuraEnabled
+     public Integer counter { get; set; } // Compliant - Public getter is accessible to Lightning components
+ 
+     @AuraEnabled
+     public static Foo bar()
+     {
+         Foo foo = new Foo();
+         foo.counter = 2; 
+         return foo;
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + LocalVariableNamingConventions + Local variable naming conventions + category/apex/codestyle.xml/LocalVariableNamingConventions + BLOCKER + Configurable naming conventions for local variable declarations. + This rule reports variable declarations which do not match the regex that applies to their + specific kind (e.g. local variable, or final local variable). Each regex can be configured through + properties.

+

By default this rule uses the standard Apex naming convention (Camel case).

+

Example

+

 public class Foo {
+     public Foo() {
+         Integer localVariable; // This is in camel case, so it's ok
+ 
+         Integer LOCAL_VARIABLE; // This will be reported unless you change the regex
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + MethodNamingConventions + Method naming conventions + category/apex/codestyle.xml/MethodNamingConventions + BLOCKER + Configurable naming conventions for method declarations. This rule reports + method declarations which do not match the regex that applies to their + specific kind (e.g. static method, or test method). Each regex can be + configured through properties.

+

By default this rule uses the standard Apex naming convention (Camel case).

+

Example

+

 public class Foo {
+     public void instanceMethod() { } // This is in camel case, so it's ok
+ 
+     public void INSTANCE_METHOD() { } // This will be reported unless you change the regex

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + MethodWithSameNameAsEnclosingClass + Method with same name as enclosing class + category/apex/errorprone.xml/MethodWithSameNameAsEnclosingClass + MAJOR + Non-constructor methods should not have the same name as the enclosing class.

+

Example

+

 public class MyClass {
+     // this is OK because it is a constructor
+     public MyClass() {}
+     // this is bad because it is a method
+     public void MyClass() {}
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + NcssConstructorCount + Ncss constructor count + category/apex/design.xml/NcssConstructorCount + MAJOR + This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines +of code for a given constructor. NCSS ignores comments, and counts actual statements. Using this algorithm, +lines of code that are split are counted as one.

+

Example

+

 public class Foo extends Bar {
+     //this constructor only has 1 NCSS lines
+     public Foo() {
+         super();
+ 
+ 
+ 
+ 
+         super.foo();
+ }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + NcssMethodCount + Ncss method count + category/apex/design.xml/NcssMethodCount + MAJOR + This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines +of code for a given method. NCSS ignores comments, and counts actual statements. Using this algorithm, +lines of code that are split are counted as one.

+

Example

+

 public class Foo extends Bar {
+     //this method only has 1 NCSS lines
+     public Integer method() {
+         super.method();
+ 
+ 
+ 
+         return 1;
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + NcssTypeCount + Ncss type count + category/apex/design.xml/NcssTypeCount + MAJOR + This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines +of code for a given type. NCSS ignores comments, and counts actual statements. Using this algorithm, +lines of code that are split are counted as one.

+

Example

+

 //this class only has 6 NCSS lines
+ public class Foo extends Bar {
+     public Foo() {
+         super();
+ 
+ 
+ 
+ 
+ 
+         super.foo();
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + OneDeclarationPerLine + One declaration per line + category/apex/codestyle.xml/OneDeclarationPerLine + BLOCKER + Apex allows the use of several variables declaration of the same type on one line. However, it +can lead to quite messy code. This rule looks for several declarations on the same line.

+

Example

+

 Integer a, b;   // not recommended
+ 
+ Integer a,
+         b;      // ok by default, can be flagged setting the strictMode property
+ 
+ Integer a;      // preferred approach
+ Integer b;

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle + + strictMode + + false + BOOLEAN + + + reportInForLoopInitializer + + true + BOOLEAN + +
+ + OperationWithHighCostInLoop + Operation with high cost in loop + category/apex/performance.xml/OperationWithHighCostInLoop + MAJOR + This rule finds method calls inside loops that are known to be likely a performance issue. These methods should be +called only once before the loop.

+

Schema class methods like Schema.getGlobalDescribe() +and Schema.describeSObjects() +might be slow depending on the size of your organization. Calling these methods repeatedly inside a loop creates +a potential performance issue.

+

Examples

+

Example 1

+

 public class GlobalDescribeExample {
+     // incorrect example
+     public void getGlobalDescribeInLoop() {
+         Set<String> fieldNameSet = new Set<String> {'Id'};
+         for (String fieldNameOrDefaultValue : fieldNameOrDefaultValueList) {
+             // Schema.getGlobalDescribe() should be called only once before the for-loop
+             if (Schema.getGlobalDescribe().get(objectName).getDescribe().fields.getMap().containsKey(fieldNameOrDefaultValue.trim())) {
+                 fieldNameSet.add(fieldNameOrDefaultValue);
+             }
+         }
+     }
+ 
+     // corrected example
+     public void getGlobalDescribeInLoopCorrected() {
+         Map<String, Schema.SObjectField> fieldMap = Schema.getGlobalDescribe().get(objectName).getDescribe().fields.getMap();
+         Set<String> fieldNameSet = new Set<String> {'Id'};
+         for (String fieldNameOrDefaultValue : fieldNameOrDefaultValueList) {
+             if (fieldMap.containsKey(fieldNameOrDefaultValue.trim())) {
+                 fieldNameSet.add(fieldNameOrDefaultValue);
+             }
+         }
+     }
+ }

+

Example 2

+

 public class DescribeSObjectsExample {
+     // incorrect example
+     public void describeSObjectsInLoop() {
+         Set<String> fieldNameSet = new Set<String> {'Id'};
+         for (String fieldNameOrDefaultValue : fieldNameOrDefaultValueList) {
+             Schema.DescribeSObjectResult dsr = Account.sObjectType.getDescribe();
+             if (Schema.describeSObjects(new List<String> { sObjectType })[0].fields.getMap().containsKey(fieldNameOrDefaultValue.trim())) {
+                 fieldNameSet.add(fieldNameOrDefaultValue);
+             }
+         }
+     }
+ 
+     // corrected example
+     public void describeSObjectsInLoop() {
+         Map<String, Schema.SObjectField> fieldMap = Schema.describeSObjects(new List<String> { 'Account' })[0].fields.getMap();
+         Set<String> fieldNameSet = new Set<String> {'Id'};
+         for (String fieldNameOrDefaultValue : fieldNameOrDefaultValueList) {
+             if (fieldMap.containsKey(fieldNameOrDefaultValue.trim())) {
+                 fieldNameSet.add(fieldNameOrDefaultValue);
+             }
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + performance +
+ + OperationWithLimitsInLoop + Operation with limits in loop + category/apex/performance.xml/OperationWithLimitsInLoop + MAJOR + Database class methods, DML operations, SOQL queries, SOSL queries, Approval class methods, Email sending, async scheduling or queueing within loops can cause governor limit exceptions. Instead, try to batch up the data into a list and invoke the operation once on that list of data outside the loop.

+

Example

+

 public class Something {
+     public void databaseMethodInsideOfLoop(List<Account> accounts) {
+         for (Account a : accounts) {
+             Database.insert(a);
+         }
+     }
+ 
+     public void dmlInsideOfLoop() {
+         for (Integer i = 0; i < 151; i++) {
+             Account account;
+             // ...
+             insert account;
+         }
+     }
+ 
+     public void soqlInsideOfLoop() {
+         for (Integer i = 0; i < 10; i++) {
+             List<Account> accounts = [SELECT Id FROM Account];
+         }
+     }
+ 
+     public void soslInsideOfLoop() {
+         for (Integer i = 0; i < 10; i++) {
+             List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead];
+         }
+     }
+ 
+     public void messageInsideOfLoop() {
+         for (Integer i = 0; i < 10; i++) {
+             Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
+             Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
+         }
+     }
+ 
+     public void approvalInsideOfLoop(Account[] accs) {
+         for (Integer i = 0; i < 10; i++) {
+             Account acc = accs[i];
+             Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
+             req.setObjectId(acc.Id);
+             Approval.process(req);
+             Approval.lock(acc);
+             Approval.unlock(acc);
+         }
+     }
+ 
+     public void asyncInsideOfLoop() {
+         for (Integer i = 0; i < 10; i++) {
+             System.enqueueJob(new MyQueueable());
+             System.schedule('x', '0 0 0 1 1 ?', new MySchedule());
+             System.scheduleBatch(new MyBatch(), 'x', 1);
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + performance +
+ + OverrideBothEqualsAndHashcode + Override both equals and hashcode + category/apex/errorprone.xml/OverrideBothEqualsAndHashcode + MAJOR + Override both public Boolean equals(Object obj), and public Integer hashCode(), or override neither. + Even if you are inheriting a hashCode() from a parent class, consider implementing hashCode and explicitly + delegating to your superclass.

+

This is especially important when Using Custom Types in Map Keys and Sets.

+

Example

+

 public class Bar {        // poor, missing a hashCode() method
+     public Boolean equals(Object o) {
+       // do some comparison
+     }
+ }
+ public class Baz {        // poor, missing an equals() method
+     public Integer hashCode() {
+       // return some hash value
+     }
+ }
+ public class Foo {        // perfect, both methods provided
+     public Boolean equals(Object other) {
+       // do some comparison
+     }
+     public Integer hashCode() {
+       // return some hash value
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + PropertyNamingConventions + Property naming conventions + category/apex/codestyle.xml/PropertyNamingConventions + BLOCKER + Configurable naming conventions for property declarations. This rule reports + property declarations which do not match the regex that applies to their + specific kind (e.g. static property, or instance property). Each regex can be + configured through properties.

+

By default this rule uses the standard Apex naming convention (Camel case).

+

Example

+

 public class Foo {
+     public Integer instanceProperty { get; set; } // This is in camel case, so it's ok
+ 
+     public Integer INSTANCE_PROPERTY { get; set; } // This will be reported unless you change the regex
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+ + QueueableWithoutFinalizer + Queueable without finalizer + category/apex/bestpractices.xml/QueueableWithoutFinalizer + INFO + Detects when the Queueable interface is used but a Finalizer is not attached. +It is best practice to call the System.attachFinalizer(Finalizer f) method within the execute method of a class which implements the Queueable interface. +Without attaching a Finalizer, there is no way of designing error recovery actions should the Queueable action fail.

+

Example

+

 // Incorrect code, does not attach a finalizer.
+ public class UserUpdater implements Queueable {
+     public List<User> usersToUpdate;
+ 
+     public UserUpdater(List<User> usersToUpdate) {
+         this.usersToUpdate = usersToUpdate;
+     }
+ 
+     public void execute(QueueableContext context) { // no Finalizer is attached
+         update usersToUpdate;
+     }
+ }
+ 
+ // Proper code, attaches a finalizer.
+ public class UserUpdater implements Queueable, Finalizer {
+     public List<User> usersToUpdate;
+ 
+     public UserUpdater(List<User> usersToUpdate) {
+         this.usersToUpdate = usersToUpdate;
+     }
+ 
+     public void execute(QueueableContext context) {
+         System.attachFinalizer(this);
+         update usersToUpdate;
+     }
+ 
+     public void execute(FinalizerContext ctx) {
+         if (ctx.getResult() == ParentJobResult.SUCCESS) {
+             // Handle success
+         } else {
+             // Handle failure
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + StdCyclomaticComplexity + Std cyclomatic complexity + category/apex/design.xml/StdCyclomaticComplexity + MAJOR + Complexity directly affects maintenance costs is determined by the number of decision points in a method +plus one for the method entry. The decision points include 'if', 'while', 'for', and 'case labels' calls. +Generally, numbers ranging from 1-4 denote low complexity, 5-7 denote moderate complexity, 8-10 denote +high complexity, and 11+ is very high complexity.

+

Example

+

 // This has a Cyclomatic Complexity = 12
+ public class Foo {
+ 1   public void example() {
+ 2   if (a == b || (c == d && e == f)) {
+ 3       if (a1 == b1) {
+             fiddle();
+ 4       } else if a2 == b2) {
+             fiddle();
+         }  else {
+             fiddle();
+         }
+ 5   } else if (c == d) {
+ 6       while (c == d) {
+             fiddle();
+         }
+ 7   } else if (e == f) {
+ 8       for (int n = 0; n < h; n++) {
+             fiddle();
+         }
+     } else {
+         switch (z) {
+ 9           case 1:
+                 fiddle();
+                 break;
+ 10          case 2:
+                 fiddle();
+                 break;
+ 11          case 3:
+                 fiddle();
+                 break;
+ 12          default:
+                 fiddle();
+                 break;
+         }
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + TestMethodsMustBeInTestClasses + Test methods must be in test classes + category/apex/errorprone.xml/TestMethodsMustBeInTestClasses + MAJOR + Test methods marked as a testMethod or annotated with @IsTest, + but not residing in a test class should be moved to a proper + class or have the @IsTest annotation added to the class.

+

Support for tests inside functional classes was removed in Spring-13 (API Version 27.0), + making classes that violate this rule fail compile-time. This rule is mostly usable when + dealing with legacy code.

+

Example

+

 // Violating
+ private class TestClass {
+   @IsTest static void myTest() {
+     // Code here
+   }
+ }
+ 
+ private class TestClass {
+   static testMethod void myTest() {
+     // Code here
+   }
+ }
+ 
+ // Compliant
+ @IsTest
+ private class TestClass {
+   @IsTest static void myTest() {
+     // Code here
+   }
+ }
+ 
+ @IsTest
+ private class TestClass {
+   static testMethod void myTest() {
+     // Code here
+   }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + TooManyFields + Too many fields + category/apex/design.xml/TooManyFields + MAJOR + Classes that have too many fields can become unwieldy and could be redesigned to have fewer fields, +possibly through grouping related fields in new objects. For example, a class with individual +city/state/zip fields could park them within a single Address field.

+

Example

+

 public class Person {
+     // too many separate fields
+     Integer birthYear;
+     Integer birthMonth;
+     Integer birthDate;
+     Double height;
+     Double weight;
+ }
+ 
+ public class Person {
+     // this is more manageable
+     Date birthDate;
+     BodyMeasurements measurements;
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + TypeShadowsBuiltInNamespace + Type shadows built in namespace + category/apex/errorprone.xml/TypeShadowsBuiltInNamespace + BLOCKER + This rule finds Apex classes, enums, and interfaces that have the same name as a class, enum, or interface in the System + or Schema namespace. + Shadowing these namespaces in this way can lead to confusion and unexpected behavior. + Code that intends to reference a System or Schema class, enum, or interface may inadvertently reference the locally defined type instead. + This can result in ambiguous code and unexpected runtime behavior. + It is best to avoid naming your types the same as those in the System or Schema namespace to prevent these issues.

+

Note that the list of classes, enums, and interfaces in the System and Schema namespaces are determined through + io.github.apex-dev-tools:standard-types. It is based on the contents of + Salesforce's Apex Reference Guide / System Namespace + and Apex Reference Guide / Schema Namespace. + As Salesforce introduces new types into the System and Schema namespaces, the rule might not always recognize + the new types and produce false-negatives und the standard types are updated.

+

Example

+

 // Violation: Causes a collision with the `System.Database` class.
+ public class Database {
+     public static String query() {
+         return 'Hello World';
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + errorprone +
+ + UnusedLocalVariable + Unused local variable + category/apex/bestpractices.xml/UnusedLocalVariable + MAJOR + Detects when a local variable is declared and/or assigned but not used.

+

Example

+

 public Boolean bar(String z) {
+         String x = 'some string'; // not used
+ 
+         String y = 'some other string'; // used in the next line
+         return z.equals(y);
+     }

+

Full documentation: PMD rule documentation

]]>
+ pmd + bestpractices +
+ + UnusedMethod + Unused method + category/apex/design.xml/UnusedMethod + MAJOR + Avoid having unused methods since they make understanding and maintaining code harder.

+

This rule finds not only unused private methods, but public methods as well, as long as +the class itself is not entirely unused. A class is considered used, if it contains at +least one other method/variable declaration that is used, as shown in the +test project file Foo.cls.

+

ApexLink is used to make this possible and this needs +additional configuration. The environment variable PMD_APEX_ROOT_DIRECTORY needs to be set prior to executing +PMD. With this variable the root directory of the Salesforce metadata, where sfdx-project.json resides, is +specified. ApexLink can then load all the classes in the project and figure out, whether a method is used or not.

+

For an accurate analysis it is important that the PMD_APEX_ROOT_DIRECTORY contains a complete set of metadata that +may be referenced from the Apex source code, such as Custom Objects, Visualforce Pages, Flows and Labels. The +PMD_APEX_ROOT_DIRECTORY directory must contain a sfdx-project.json, but metadata may be either in the +SFDX Source format +or the older MDAPI format. The packageDirectories entries in sfdx-project.json are used to determine which +directories to search for metadata, if a .forceignore file is present it will be respected.

+

If the Apex code references external packages via namespace(s) you should declare these in your sfdx-project.json +file using the 'plugins' syntax shown in the example below to avoid errors. Here's an example of a +well-formed sfdx-project.json: +

 {
+     "packageDirectories": [
+       {
+         "path": "src",
+         "default": true
+       }
+     ],
+     "namespace": "my_namespace",
+     "sfdcLoginUrl": "https://login.salesforce.com",
+     "sourceApiVersion": "52.0",
+     "plugins": {
+         "dependencies": [
+             {"namespace": "aa"}
+         ]
+     }
+ }

+

Example

+

 public class Triangle {
+     private Double side1;
+     private Double side2;
+     private Double side3;
+ 
+     public Triangle(Double side1, Double side2, Double side3) {
+         this.side1 = side1;
+         this.side2 = side2;
+         this.side3 = side3;
+     }
+ 
+     // Method is not invoked so can be removed
+     public Double area() {
+         return (side1 + side2 + side3)/2;
+     }
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + design +
+ + WhileLoopsMustUseBraces + While loops must use braces + category/apex/codestyle.xml/WhileLoopsMustUseBraces + MAJOR + Avoid using 'while' statements without using braces to surround the code block. If the code +formatting or indentation is lost then it becomes difficult to separate the code being +controlled from the rest.

+

Example

+

 while (true)    // not recommended
+     x++;
+ 
+ while (true) {  // preferred approach
+     x++;
+ }

+

Full documentation: PMD rule documentation

]]>
+ pmd + codestyle +
+
\ No newline at end of file diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java index 2643239a..de135ec2 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java @@ -26,6 +26,7 @@ import org.sonar.api.SonarRuntime; import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.utils.Version; +import org.sonar.plugins.pmd.rule.PmdApexRulesDefinition; import org.sonar.plugins.pmd.rule.PmdKotlinRulesDefinition; import org.sonar.plugins.pmd.rule.PmdRulesDefinition; @@ -51,13 +52,14 @@ void testPluginConfiguration() { // then final List extensions = context.getExtensions(); assertThat(extensions) - .hasSize(7) + .hasSize(8) .contains( PmdSensor.class, PmdConfiguration.class, PmdExecutor.class, PmdRulesDefinition.class, PmdKotlinRulesDefinition.class, + PmdApexRulesDefinition.class, PmdViolationRecorder.class ); } diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java index c4089265..5a9fff19 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java @@ -181,13 +181,13 @@ void whenDescribeCalledThenSensorDescriptionIsWritten() { // given final SensorDescriptor mockDescriptor = mock(SensorDescriptor.class); - when(mockDescriptor.onlyOnLanguages(anyString(), anyString())).thenReturn(mockDescriptor); + when(mockDescriptor.onlyOnLanguages(anyString(), anyString(), anyString())).thenReturn(mockDescriptor); // when pmdSensor.describe(mockDescriptor); // then - verify(mockDescriptor).onlyOnLanguages(PmdConstants.LANGUAGE_JAVA_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY); + verify(mockDescriptor).onlyOnLanguages(PmdConstants.LANGUAGE_JAVA_KEY, PmdConstants.LANGUAGE_KOTLIN_KEY, PmdConstants.LANGUAGE_APEX_KEY); verify(mockDescriptor).name("PmdSensor"); } From 28dd59d6cd5998e182e0d81c82c79befc0f8adaf Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 4 Jul 2025 11:23:03 +0200 Subject: [PATCH 02/10] added PMD Apex rules - attempt to activate scanning of apex files --- pom.xml | 2 +- .../org/sonar/plugins/pmd/PmdExecutor.java | 57 +++++++++--- .../sonar/plugins/pmd/PmdExecutorFactory.java | 93 +++++++++++++++++++ .../java/org/sonar/plugins/pmd/PmdPlugin.java | 11 ++- .../java/org/sonar/plugins/pmd/PmdSensor.java | 9 +- .../org/sonar/plugins/pmd/PmdTemplate.java | 91 ++++++++++++++++-- .../plugins/pmd/languages/ApexLanguage.java | 50 ++++++++++ .../pmd/languages/ApexLanguageProperties.java | 54 +++++++++++ .../pmd/profile/PmdApexSonarWayProfile.java | 50 ++++++++++ .../plugins/pmd/PmdExecutorFactoryTest.java | 62 +++++++++++++ .../org/sonar/plugins/pmd/PmdPluginTest.java | 10 +- .../org/sonar/plugins/pmd/PmdSensorTest.java | 6 +- .../sonar/plugins/pmd/PmdTemplateTest.java | 2 +- 13 files changed, 463 insertions(+), 34 deletions(-) create mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java create mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java create mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java create mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java create mode 100644 sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java diff --git a/pom.xml b/pom.xml index dc13c5e7..2017ea39 100644 --- a/pom.xml +++ b/pom.xml @@ -340,7 +340,7 @@ ${buildNumber} ${timestamp} - java + java,apex,kotlin diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java index 24372be0..12c1d4a7 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutor.java @@ -71,6 +71,16 @@ public PmdExecutor(FileSystem fileSystem, ActiveRules rulesProfile, this.settings = settings; } + // Constructor without JavaResourceLocator for non-Java analysis + public PmdExecutor(FileSystem fileSystem, ActiveRules rulesProfile, + PmdConfiguration pmdConfiguration, Configuration settings) { + this.fs = fileSystem; + this.rulesProfile = rulesProfile; + this.pmdConfiguration = pmdConfiguration; + this.javaResourceLocator = null; + this.settings = settings; + } + private static void accept(FileAnalysisListener fal) { LOGGER.debug("Got FileAnalysisListener: {}", fal); } @@ -96,12 +106,28 @@ public Report execute() { private Report executePmd(URLClassLoader classLoader) { final PmdTemplate pmdFactory = createPmdTemplate(classLoader); - final Optional javaMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_JAVA_KEY), PmdConstants.MAIN_JAVA_REPOSITORY_KEY); - final Optional javaTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_JAVA_KEY), PmdConstants.MAIN_JAVA_REPOSITORY_KEY); - final Optional kotlinMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY); - final Optional kotlinTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY); - final Optional apexMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); - final Optional apexTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); + + // Initialize reports as empty + Optional javaMainReport = Optional.empty(); + Optional javaTestReport = Optional.empty(); + Optional kotlinMainReport = Optional.empty(); + Optional kotlinTestReport = Optional.empty(); + Optional apexMainReport = Optional.empty(); + Optional apexTestReport = Optional.empty(); + + // Only analyze Java files if JavaResourceLocator is available + if (javaResourceLocator != null) { + javaMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_JAVA_KEY), PmdConstants.MAIN_JAVA_REPOSITORY_KEY); + javaTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_JAVA_KEY), PmdConstants.MAIN_JAVA_REPOSITORY_KEY); + } else { + LOGGER.info("Skipping Java analysis because JavaResourceLocator is not available"); + } + + // Always analyze Kotlin and Apex files + kotlinMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY); + kotlinTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY); + apexMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); + apexTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); if (LOGGER.isDebugEnabled()) { javaMainReport.ifPresent(this::writeDebugLine); @@ -201,15 +227,22 @@ PmdTemplate createPmdTemplate(URLClassLoader classLoader) { * @return A classloader for PMD that contains all dependencies of the project that shall be analyzed. */ private URLClassLoader createClassloader() { - Collection classpathElements = javaResourceLocator.classpath(); List urls = new ArrayList<>(); - for (File file : classpathElements) { - try { - urls.add(file.toURI().toURL()); - } catch (MalformedURLException e) { - throw new IllegalStateException("Failed to create the project classloader. Classpath element is invalid: " + file, e); + + // Only try to get classpath elements if JavaResourceLocator is available + if (javaResourceLocator != null) { + Collection classpathElements = javaResourceLocator.classpath(); + for (File file : classpathElements) { + try { + urls.add(file.toURI().toURL()); + } catch (MalformedURLException e) { + throw new IllegalStateException("Failed to create the project classloader. Classpath element is invalid: " + file, e); + } } + } else { + LOGGER.debug("JavaResourceLocator not available, using empty classpath"); } + return new URLClassLoader(urls.toArray(new URL[0])); } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java new file mode 100644 index 00000000..6c5ec8ce --- /dev/null +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java @@ -0,0 +1,93 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd; + +import org.sonar.api.batch.ScannerSide; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.plugins.java.api.JavaResourceLocator; + +/** + * Factory for creating PmdExecutor instances. + * This class handles the case where JavaResourceLocator is not available + * (e.g., when analyzing non-Java projects). + */ +@ScannerSide +public class PmdExecutorFactory { + + private static final Logger LOGGER = Loggers.get(PmdExecutorFactory.class); + + private final FileSystem fileSystem; + private final ActiveRules activeRules; + private final PmdConfiguration pmdConfiguration; + private final Configuration settings; + private final JavaResourceLocator javaResourceLocator; + + /** + * Constructor with all dependencies including JavaResourceLocator. + * This constructor will be used by Sonar's dependency injection system + * when JavaResourceLocator is available. + */ + public PmdExecutorFactory(FileSystem fileSystem, ActiveRules activeRules, + PmdConfiguration pmdConfiguration, Configuration settings, + JavaResourceLocator javaResourceLocator) { + this.fileSystem = fileSystem; + this.activeRules = activeRules; + this.pmdConfiguration = pmdConfiguration; + this.settings = settings; + this.javaResourceLocator = javaResourceLocator; + LOGGER.debug("PmdExecutorFactory created with JavaResourceLocator"); + } + + /** + * Constructor without JavaResourceLocator. + * This constructor will be used by Sonar's dependency injection system + * when JavaResourceLocator is not available. + */ + public PmdExecutorFactory(FileSystem fileSystem, ActiveRules activeRules, + PmdConfiguration pmdConfiguration, Configuration settings) { + this.fileSystem = fileSystem; + this.activeRules = activeRules; + this.pmdConfiguration = pmdConfiguration; + this.settings = settings; + this.javaResourceLocator = null; + LOGGER.debug("PmdExecutorFactory created without JavaResourceLocator"); + } + + /** + * Creates a PmdExecutor instance. + * If JavaResourceLocator is available, it will be passed to the PmdExecutor. + * Otherwise, a PmdExecutor without JavaResourceLocator will be created. + * + * @return A PmdExecutor instance + */ + public PmdExecutor create() { + if (javaResourceLocator != null) { + LOGGER.debug("Creating PmdExecutor with JavaResourceLocator"); + return new PmdExecutor(fileSystem, activeRules, pmdConfiguration, javaResourceLocator, settings); + } else { + LOGGER.debug("Creating PmdExecutor without JavaResourceLocator"); + return new PmdExecutor(fileSystem, activeRules, pmdConfiguration, settings); + } + } +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java index 1cce3a38..f019e485 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java @@ -21,6 +21,9 @@ import org.sonar.api.Plugin; import org.sonar.api.config.PropertyDefinition; +import org.sonar.plugins.pmd.languages.ApexLanguage; +import org.sonar.plugins.pmd.languages.ApexLanguageProperties; +import org.sonar.plugins.pmd.profile.PmdApexSonarWayProfile; import org.sonar.plugins.pmd.rule.PmdApexRulesDefinition; import org.sonar.plugins.pmd.rule.PmdKotlinRulesDefinition; import org.sonar.plugins.pmd.rule.PmdRulesDefinition; @@ -41,11 +44,15 @@ public void define(Context context) { PmdSensor.class, PmdConfiguration.class, - PmdExecutor.class, + PmdExecutorFactory.class, PmdRulesDefinition.class, PmdKotlinRulesDefinition.class, PmdApexRulesDefinition.class, - PmdViolationRecorder.class + PmdViolationRecorder.class, + ApexLanguage.class, + PmdApexSonarWayProfile.class ); + + context.addExtensions(ApexLanguageProperties.getProperties()); } } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java index bda4e0f2..68480786 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java @@ -27,16 +27,18 @@ import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.plugins.pmd.PmdExecutor; +import org.sonar.plugins.pmd.PmdExecutorFactory; public class PmdSensor implements Sensor { private final ActiveRules profile; - private final PmdExecutor executor; + private final PmdExecutorFactory executorFactory; private final PmdViolationRecorder pmdViolationRecorder; private final FileSystem fs; - public PmdSensor(ActiveRules profile, PmdExecutor executor, PmdViolationRecorder pmdViolationRecorder, FileSystem fs) { + public PmdSensor(ActiveRules profile, PmdExecutorFactory executorFactory, PmdViolationRecorder pmdViolationRecorder, FileSystem fs) { this.profile = profile; - this.executor = executor; + this.executorFactory = executorFactory; this.pmdViolationRecorder = pmdViolationRecorder; this.fs = fs; } @@ -73,6 +75,7 @@ public void describe(SensorDescriptor descriptor) { @Override public void execute(SensorContext context) { if (shouldExecuteOnProject()) { + PmdExecutor executor = executorFactory.create(); for (RuleViolation violation : executor.execute().getViolations()) { pmdViolationRecorder.saveViolation(violation, context); } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java index 24e8b838..42f93775 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java @@ -20,8 +20,12 @@ package org.sonar.plugins.pmd; import net.sourceforge.pmd.*; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.apex.ApexLanguageModule; import net.sourceforge.pmd.lang.java.JavaLanguageModule; +import net.sourceforge.pmd.lang.kotlin.KotlinLanguageModule; import net.sourceforge.pmd.lang.rule.RuleSet; import net.sourceforge.pmd.renderers.EmptyRenderer; import net.sourceforge.pmd.reporting.Report; @@ -69,7 +73,12 @@ private static Map prepareVersions() { public static PmdTemplate create(String javaVersion, ClassLoader classloader, Charset charset) { PMDConfiguration configuration = new PMDConfiguration(); - configuration.setDefaultLanguageVersion(languageVersion(javaVersion)); + + // Set Java as the default language version + LanguageVersion javaLanguageVersion = languageVersion(PmdConstants.LANGUAGE_JAVA_KEY, javaVersion); + configuration.setDefaultLanguageVersion(javaLanguageVersion); + LOG.info("Set default language version to Java: " + javaLanguageVersion.getName()); + configuration.setClassLoader(classloader); configuration.setSourceEncoding(charset); configuration.setFailOnViolation(false); @@ -79,14 +88,27 @@ public static PmdTemplate create(String javaVersion, ClassLoader classloader, Ch return new PmdTemplate(configuration); } - static LanguageVersion languageVersion(String javaVersion) { - String version = normalize(javaVersion); - LanguageVersion languageVersion = new JavaLanguageModule().getVersion(version); - if (languageVersion == null) { - throw new IllegalArgumentException("Unsupported Java version for PMD: " + version); + static LanguageVersion languageVersion(String languageKey, String version) { + if (PmdConstants.LANGUAGE_JAVA_KEY.equals(languageKey)) { + String normalizedVersion = normalize(version); + LanguageVersion languageVersion = new JavaLanguageModule().getVersion(normalizedVersion); + if (languageVersion == null) { + throw new IllegalArgumentException("Unsupported Java version for PMD: " + normalizedVersion); + } + LOG.info("Java version: " + normalizedVersion); + return languageVersion; + } else if (PmdConstants.LANGUAGE_APEX_KEY.equals(languageKey)) { + LanguageVersion languageVersion = new ApexLanguageModule().getDefaultVersion(); + LOG.info("Using Apex default version"); + return languageVersion; + } else if (PmdConstants.LANGUAGE_KOTLIN_KEY.equals(languageKey)) { + LanguageVersion languageVersion = new KotlinLanguageModule().getDefaultVersion(); + LOG.info("Using Kotlin default version"); + return languageVersion; } - LOG.info("Java version: " + version); - return languageVersion; + + // Default to Java + return new JavaLanguageModule().getDefaultVersion(); } private static String normalize(String version) { @@ -100,9 +122,58 @@ PMDConfiguration configuration() { public Report process(Iterable files, RuleSet ruleset) { try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { pmd.addRuleSet(ruleset); - for (InputFile file: files) { - pmd.files().addFile(Paths.get(file.uri())); + + // Group files by language + Map> filesByLanguage = new HashMap<>(); + for (InputFile file : files) { + String language = file.language(); + if (language == null) { + // Try to determine language from file extension + String filename = file.filename(); + if (filename.endsWith(".cls") || filename.endsWith(".trigger")) { + language = PmdConstants.LANGUAGE_APEX_KEY; + } else if (filename.endsWith(".kt") || filename.endsWith(".kts")) { + language = PmdConstants.LANGUAGE_KOTLIN_KEY; + } else { + // Default to Java + language = PmdConstants.LANGUAGE_JAVA_KEY; + } + } + + filesByLanguage.computeIfAbsent(language, k -> new ArrayList<>()).add(file); } + + // Process files by language + for (Map.Entry> entry : filesByLanguage.entrySet()) { + String language = entry.getKey(); + List languageFiles = entry.getValue(); + + if (!languageFiles.isEmpty()) { + LOG.info("Processing {} files with language: {}", languageFiles.size(), language); + + // Set the appropriate language version for this batch of files + if (PmdConstants.LANGUAGE_APEX_KEY.equals(language)) { + LanguageVersion apexVersion = new ApexLanguageModule().getDefaultVersion(); + configuration.setDefaultLanguageVersion(apexVersion); + LOG.info("Set language version to Apex: {}", apexVersion.getName()); + } else if (PmdConstants.LANGUAGE_KOTLIN_KEY.equals(language)) { + LanguageVersion kotlinVersion = new KotlinLanguageModule().getDefaultVersion(); + configuration.setDefaultLanguageVersion(kotlinVersion); + LOG.info("Set language version to Kotlin: {}", kotlinVersion.getName()); + } else { + // Default to Java + LanguageVersion javaVersion = languageVersion(PmdConstants.LANGUAGE_JAVA_KEY, "24"); + configuration.setDefaultLanguageVersion(javaVersion); + LOG.info("Set language version to Java: {}", javaVersion.getName()); + } + + // Add files to PMD + for (InputFile file : languageFiles) { + pmd.files().addFile(Paths.get(file.uri())); + } + } + } + return pmd.performAnalysisAndCollectReport(); } } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java new file mode 100644 index 00000000..0a4d13fb --- /dev/null +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java @@ -0,0 +1,50 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd.languages; + +import org.sonar.api.config.Configuration; +import org.sonar.api.resources.AbstractLanguage; +import org.sonar.plugins.pmd.PmdConstants; + +/** + * This class defines the Apex language. + */ +public final class ApexLanguage extends AbstractLanguage { + + private final Configuration config; + + /** + * Creates a new Apex language instance. + * @param config The SonarQube configuration + */ + public ApexLanguage(Configuration config) { + super(PmdConstants.LANGUAGE_APEX_KEY, PmdConstants.REPOSITORY_APEX_NAME); + this.config = config; + } + + @Override + public String[] getFileSuffixes() { + String[] suffixes = config.getStringArray(ApexLanguageProperties.FILE_SUFFIXES_KEY); + if (suffixes == null || suffixes.length == 0) { + suffixes = ApexLanguageProperties.DEFAULT_FILE_SUFFIXES.split(","); + } + return suffixes; + } +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java new file mode 100644 index 00000000..47aa8d8b --- /dev/null +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java @@ -0,0 +1,54 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd.languages; + +import org.sonar.api.config.PropertyDefinition; +import org.sonar.plugins.pmd.PmdConstants; + +import java.util.List; + +/** + * Properties for the Apex language. + */ +public class ApexLanguageProperties { + + public static final String FILE_SUFFIXES_KEY = "sonar.apex.file.suffixes"; + public static final String DEFAULT_FILE_SUFFIXES = ".cls,.trigger"; + + private ApexLanguageProperties() { + // only statics + } + + /** + * Creates a list of property definitions for Apex language. + * @return List of property definitions + */ + public static List getProperties() { + return List.of( + PropertyDefinition.builder(FILE_SUFFIXES_KEY) + .defaultValue(DEFAULT_FILE_SUFFIXES) + .name("File Suffixes") + .description("Comma-separated list of suffixes for files to analyze.") + .category(PmdConstants.PLUGIN_NAME) + .subCategory("Apex") + .build() + ); + } +} diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java new file mode 100644 index 00000000..9cbea2d7 --- /dev/null +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java @@ -0,0 +1,50 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd.profile; + +import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; +import org.sonar.plugins.pmd.PmdConstants; +import org.sonar.plugins.pmd.rule.PmdApexRulesDefinition; + +/** + * Defines a built-in quality profile for Apex language. + */ +public class PmdApexSonarWayProfile implements BuiltInQualityProfilesDefinition { + + @Override + public void define(Context context) { + NewBuiltInQualityProfile profile = context.createBuiltInQualityProfile("Sonar way", PmdConstants.LANGUAGE_APEX_KEY); + profile.setDefault(true); + + // Add a few rules to the profile + // These are just examples, you can add more rules as needed + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "AvoidDebugStatements"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "OperationWithLimitsInLoop"); // Covers both DML and SOQL in loops + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "AvoidGlobalModifier"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "AvoidLogicInTrigger"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "AvoidNonExistentAnnotations"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "ClassNamingConventions"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "MethodNamingConventions"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "PropertyNamingConventions"); + profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "LocalVariableNamingConventions"); + + profile.done(); + } +} diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java new file mode 100644 index 00000000..ef70c30e --- /dev/null +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java @@ -0,0 +1,62 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd; + +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.config.Configuration; +import org.sonar.plugins.java.api.JavaResourceLocator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class PmdExecutorFactoryTest { + + private final FileSystem fs = mock(FileSystem.class); + private final ActiveRules activeRules = mock(ActiveRules.class); + private final PmdConfiguration pmdConfiguration = mock(PmdConfiguration.class); + private final Configuration settings = mock(Configuration.class); + private final JavaResourceLocator javaResourceLocator = mock(JavaResourceLocator.class); + + @Test + void should_create_executor_with_java_resource_locator_when_available() { + // given + PmdExecutorFactory factory = new PmdExecutorFactory(fs, activeRules, pmdConfiguration, settings, javaResourceLocator); + + // when + PmdExecutor executor = factory.create(); + + // then + assertThat(executor).isNotNull(); + } + + @Test + void should_create_executor_without_java_resource_locator_when_not_available() { + // given + PmdExecutorFactory factory = new PmdExecutorFactory(fs, activeRules, pmdConfiguration, settings); + + // when + PmdExecutor executor = factory.create(); + + // then + assertThat(executor).isNotNull(); + } +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java index de135ec2..046412b4 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java @@ -26,6 +26,8 @@ import org.sonar.api.SonarRuntime; import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.utils.Version; +import org.sonar.plugins.pmd.languages.ApexLanguage; +import org.sonar.plugins.pmd.profile.PmdApexSonarWayProfile; import org.sonar.plugins.pmd.rule.PmdApexRulesDefinition; import org.sonar.plugins.pmd.rule.PmdKotlinRulesDefinition; import org.sonar.plugins.pmd.rule.PmdRulesDefinition; @@ -52,15 +54,17 @@ void testPluginConfiguration() { // then final List extensions = context.getExtensions(); assertThat(extensions) - .hasSize(8) + .hasSize(11) // 8 original extensions + ApexLanguage + 1 property definition + PmdApexSonarWayProfile .contains( PmdSensor.class, PmdConfiguration.class, - PmdExecutor.class, + PmdExecutorFactory.class, PmdRulesDefinition.class, PmdKotlinRulesDefinition.class, PmdApexRulesDefinition.class, - PmdViolationRecorder.class + PmdViolationRecorder.class, + ApexLanguage.class, + PmdApexSonarWayProfile.class ); } diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java index 5a9fff19..d4f3cd73 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java @@ -43,6 +43,7 @@ class PmdSensorTest { private final ActiveRules profile = mock(ActiveRules.class, RETURNS_DEEP_STUBS); private final PmdExecutor executor = mock(PmdExecutor.class); + private final PmdExecutorFactory executorFactory = mock(PmdExecutorFactory.class); private final PmdViolationRecorder pmdViolationRecorder = mock(PmdViolationRecorder.class); private final SensorContext sensorContext = mock(SensorContext.class); private final DefaultFileSystem fs = new DefaultFileSystem(new File(".")); @@ -51,7 +52,8 @@ class PmdSensorTest { @BeforeEach void setUpPmdSensor() { - pmdSensor = new PmdSensor(profile, executor, pmdViolationRecorder, fs); + when(executorFactory.create()).thenReturn(executor); + pmdSensor = new PmdSensor(profile, executorFactory, pmdViolationRecorder, fs); } @Test @@ -222,4 +224,4 @@ private void addOneJavaFile(Type type) { .build() ); } -} +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdTemplateTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdTemplateTest.java index 02bdebc1..a7cc882e 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdTemplateTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdTemplateTest.java @@ -38,7 +38,7 @@ class PmdTemplateTest { }) void verifyCanHandleJavaLanguageVersion(String javaVersion) { final Language language = PmdTemplate - .languageVersion(javaVersion) + .languageVersion(PmdConstants.LANGUAGE_JAVA_KEY, javaVersion) .getLanguage(); assertThat(language).isNotNull(); From e68829185864b3113494391a4092c2355a9801da Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 15 Jul 2025 15:31:16 +0200 Subject: [PATCH 03/10] integration-test: add Kotlin IT tests --- .junie/guidelines.md | 62 ++++++++ .../projects/pmd-kotlin-rules/pom.xml | 63 ++++++++ .../main/kotlin/com/example/KotlinErrors.kt | 19 +++ .../examples/pmd/PmdExtensionRepository.java | 2 +- .../com/sonar/it/java/suite/PmdKotlinIT.java | 147 ++++++++++++++++++ .../orchestrator/PmdTestOrchestrator.java | 18 ++- .../it/java/PmdTest/pmd-kotlin-all-rules.xml | 17 ++ .../it/java/PmdTest/pmd-kotlin-profile.xml | 17 ++ 8 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 .junie/guidelines.md create mode 100644 integration-test/projects/pmd-kotlin-rules/pom.xml create mode 100644 integration-test/projects/pmd-kotlin-rules/src/main/kotlin/com/example/KotlinErrors.kt create mode 100644 integration-test/src/test/java/com/sonar/it/java/suite/PmdKotlinIT.java create mode 100644 integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-all-rules.xml create mode 100644 integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-profile.xml diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 00000000..74cb3b1b --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,62 @@ +# Sonar-PMD Plugin Guidelines + +## Project Overview +Sonar-PMD is a SonarQube plugin that integrates PMD (a static code analyzer) into SonarQube. It provides coding rules from PMD for use in SonarQube, allowing users to detect code quality issues in their Java, Apex, and Kotlin code. + +The project is currently maintained by Jeroen Borgers and Peter Paul Bakker, and is sponsored by Rabobank. It was previously maintained by SonarSource and later by Jens Gerdes before being transferred to the current maintainers in 2022. + +## Project Structure +The project is organized as a multi-module Maven project with the following modules: + +1. **sonar-pmd-lib**: Core library containing the PMD rule definitions and integration logic +2. **sonar-pmd-plugin**: The actual SonarQube plugin that gets packaged and deployed +3. **integration-test**: Integration tests for the plugin + +Key directories: +- `/sonar-pmd-lib/src/main/java`: Core implementation classes +- `/sonar-pmd-plugin/src/main/java`: Plugin-specific implementation +- `/integration-test/src/test/java`: Integration tests + +## Build Requirements +- Java 17 is required to build the plugin +- Maven 3.8+ is required + +## Testing Guidelines +When making changes to the plugin, Junie should: + +1. **Run unit tests** to verify that the changes don't break existing functionality: + ``` + ./mvnw clean test + ``` + +2. **Run integration tests** for more comprehensive testing: + ``` + ./mvnw clean verify + ``` + +3. **Test with different SonarQube versions** if making changes that might affect compatibility. The plugin currently supports SonarQube 9.9.4 and above. + +## Code Style +The project follows standard Java code style conventions. When making changes: + +1. Keep code clean and readable +2. Add appropriate JavaDoc comments for public classes and methods +3. Follow existing patterns in the codebase +4. Ensure backward compatibility when possible + +## Version Compatibility +The plugin has specific version compatibility requirements: +- PMD version: 7.15.0 +- Java source compatibility: 8 to 24 (including 24-preview) +- SonarQube compatibility: 9.9.4 and above + +## Release Process +The project uses semantic versioning: +- Major version changes indicate breaking changes (e.g., PMD 6 to PMD 7) +- Minor version changes indicate new features +- Patch version changes indicate bug fixes + +## Important Notes +1. A number of the PMD rules have been rewritten in the default Sonar Java plugin. For known alternatives, the `has-sonar-alternative` tag is added with references to these alternative(s). +2. The plugin is licensed under the GNU Lesser General Public License, Version 3.0. +3. Parts of the rule descriptions displayed in SonarQube have been extracted from PMD and are licensed under a BSD-style license. diff --git a/integration-test/projects/pmd-kotlin-rules/pom.xml b/integration-test/projects/pmd-kotlin-rules/pom.xml new file mode 100644 index 00000000..3e827da2 --- /dev/null +++ b/integration-test/projects/pmd-kotlin-rules/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + + com.sonarsource.it.projects + pmd-kotlin-rules + 1.0-SNAPSHOT + + 1.9.0 + UTF-8 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + ${project.basedir}/src/main/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 5.1.0.4751 + + + + + + + + skipSonar + + + skipTestProjects + true + + + + true + + + + diff --git a/integration-test/projects/pmd-kotlin-rules/src/main/kotlin/com/example/KotlinErrors.kt b/integration-test/projects/pmd-kotlin-rules/src/main/kotlin/com/example/KotlinErrors.kt new file mode 100644 index 00000000..5d8881cd --- /dev/null +++ b/integration-test/projects/pmd-kotlin-rules/src/main/kotlin/com/example/KotlinErrors.kt @@ -0,0 +1,19 @@ +package com.example + +class KotlinErrors { + // This function name is too short, which will trigger the FunctionNameTooShort rule + fun fn() { + println("This function name is too short") + } +} + +// This class overrides equals but not hashCode, which will trigger the OverrideBothEqualsAndHashcode rule +class EqualsOnly { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return true + } + + // Missing hashCode() override +} \ No newline at end of file diff --git a/integration-test/src/main/java/org/sonar/examples/pmd/PmdExtensionRepository.java b/integration-test/src/main/java/org/sonar/examples/pmd/PmdExtensionRepository.java index 4eb28706..6aebbb8e 100644 --- a/integration-test/src/main/java/org/sonar/examples/pmd/PmdExtensionRepository.java +++ b/integration-test/src/main/java/org/sonar/examples/pmd/PmdExtensionRepository.java @@ -33,7 +33,7 @@ public class PmdExtensionRepository implements RulesDefinition { private static final Logger LOGGER = LoggerFactory.getLogger(PmdExtensionRepository.class); - // Must be the same than the PMD plugin + // Must be the same as the PMD plugin private static final String REPOSITORY_KEY = "pmd"; private static final String LANGUAGE_KEY = "java"; diff --git a/integration-test/src/test/java/com/sonar/it/java/suite/PmdKotlinIT.java b/integration-test/src/test/java/com/sonar/it/java/suite/PmdKotlinIT.java new file mode 100644 index 00000000..41c8429d --- /dev/null +++ b/integration-test/src/test/java/com/sonar/it/java/suite/PmdKotlinIT.java @@ -0,0 +1,147 @@ +/* + * SonarQube PMD7 Plugin Integration Test + * Copyright (C) 2013-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package com.sonar.it.java.suite; + +import com.sonar.it.java.suite.orchestrator.PmdTestOrchestrator; +import com.sonar.orchestrator.build.BuildResult; +import com.sonar.orchestrator.build.MavenBuild; +import com.sonar.orchestrator.http.HttpException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.sonar.wsclient.issue.Issue; +import org.sonar.wsclient.issue.IssueQuery; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.sonar.it.java.suite.TestUtils.keyFor; +import static org.assertj.core.api.Assertions.assertThat; + +class PmdKotlinIT { + + private static PmdTestOrchestrator ORCHESTRATOR; + + @BeforeAll + static void startSonar() { + ORCHESTRATOR = PmdTestOrchestrator.init(); + ORCHESTRATOR.start(); + } + + @Test + void testKotlinRules() { + // given + final String projectName = "pmd-kotlin-rules"; + final MavenBuild build = MavenBuild + .create(TestUtils.projectPom(projectName)) + .setCleanSonarGoals() + .setProperty("sonar.kotlin.file.suffixes", ".kt") + .setProperty("sonar.sources", "src/main/kotlin") + .setProperty("sonar.java.binaries", "target/classes") + .setProperty("sonar.log.level", "DEBUG") + .setProperty("sonar.verbose", "true"); + + try { + ORCHESTRATOR.associateProjectToQualityProfile("pmd-kotlin-profile", projectName, "kotlin"); + + // when + final BuildResult buildResult = ORCHESTRATOR.executeBuild(build); + + // then + final String log = buildResult.getLogs(); + assertThat(log).contains("Kotlin"); + + System.out.println("[DEBUG_LOG] Build log: " + log); + + final List issues = retrieveIssues(keyFor(projectName, "com/example/", "KotlinErrors")); + System.out.println("[DEBUG_LOG] Issues found: " + issues.size()); + + // Also check for issues on EqualsOnly class specifically + final List equalsOnlyIssues = retrieveIssues(keyFor(projectName, "com/example/", "EqualsOnly")); + System.out.println("[DEBUG_LOG] EqualsOnly issues found: " + equalsOnlyIssues.size()); + + final List messages = issues + .stream() + .map(Issue::message) + .collect(Collectors.toList()); + + System.out.println("[DEBUG_LOG] Messages: " + messages); + + assertThat(issues).isNotEmpty(); + + assertThat(messages) + .contains( + "Function names should have non-cryptic and clear names.", + "Ensure you override both equals() and hashCode()" + ); + } catch (HttpException e) { + System.out.println("Failed to associate Project To Quality Profile: " + e.getMessage() + " body: " + e.getBody()); + throw e; + } finally { + // Cleanup + ORCHESTRATOR.resetData(projectName); + } + } + + @Test + void pmdKotlinShouldRunWithAllRulesEnabled() { + // given + final String projectName = "pmd-kotlin-rules"; + final MavenBuild build = MavenBuild + .create(TestUtils.projectPom(projectName)) + .setCleanPackageSonarGoals() + .setProperty("sonar.kotlin.file.suffixes", ".kt") + .setProperty("sonar.sources", "src/main/kotlin") + .setProperty("sonar.java.binaries", "target/classes") + .setProperty("sonar.log.level", "DEBUG") + .setProperty("sonar.verbose", "true"); + try { + ORCHESTRATOR.associateProjectToQualityProfile("pmd-kotlin-all-rules", projectName, "kotlin"); + + // when + final BuildResult buildResult = ORCHESTRATOR.executeBuild(build); + + // then + final String log = buildResult.getLogs(); + System.out.println("[DEBUG_LOG] Build log: " + log); + + final List issues = retrieveIssues(keyFor(projectName, "com/example/", "KotlinErrors")); + System.out.println("[DEBUG_LOG] Issues found: " + issues.size()); + + // Also check for issues on EqualsOnly class specifically + final List equalsOnlyIssues = retrieveIssues(keyFor(projectName, "com/example/", "EqualsOnly")); + System.out.println("[DEBUG_LOG] EqualsOnly issues found: " + equalsOnlyIssues.size()); + + assertThat(issues).isNotEmpty(); + + } catch (HttpException e) { + System.out.println("Failed to associate Project To Quality Profile: " + e.getMessage() + " body: " + e.getBody()); + throw e; + } finally { + // Cleanup + ORCHESTRATOR.resetData(projectName); + } + } + + private List retrieveIssues(String componentKey) { + final IssueQuery issueQuery = IssueQuery.create(); + issueQuery.urlParams().put("componentKeys", componentKey); + return ORCHESTRATOR.retrieveIssues(issueQuery); + } +} diff --git a/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java b/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java index 2b6f5673..9a06d37c 100644 --- a/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java +++ b/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java @@ -41,6 +41,7 @@ public class PmdTestOrchestrator { private static final String SONAR_JAVA_PLUGIN_VERSION_KEY = "test.sonar.plugin.version.java"; + private static final String SONAR_KOTLIN_PLUGIN_VERSION_KEY = "test.sonar.plugin.version.kotlin"; private static final String SONAR_VERSION_KEY = "test.sonar.version"; private static final String LANGUAGE_KEY = "java"; private static final String CENTRAL_MAVEN = "https://repo1.maven.org/maven2"; @@ -87,9 +88,13 @@ public List retrieveIssues(IssueQuery query) { } public void associateProjectToQualityProfile(String profile, String project) { + associateProjectToQualityProfile(profile, project, LANGUAGE_KEY); + } + + public void associateProjectToQualityProfile(String profile, String project, String language) { final String projectKey = deriveProjectKey(project); delegate.getServer().provisionProject(projectKey, project); - delegate.getServer().associateProjectToQualityProfile(projectKey, LANGUAGE_KEY, profile); + delegate.getServer().associateProjectToQualityProfile(projectKey, language, profile); } public static PmdTestOrchestrator init() { @@ -102,12 +107,19 @@ public static PmdTestOrchestrator init() { "sonar-java-plugin", determineJavaPluginVersion() )) + .addPlugin(MavenLocation.create( + "org.sonarsource.kotlin", + "sonar-kotlin-plugin", + determineKotlinPluginVersion() + )) .addPlugin(byWildcardMavenFilename(new File("../sonar-pmd-plugin/target"), "sonar-pmd-plugin-*.jar")) .addPlugin(byWildcardMavenFilename(new File("./target"), "integration-test-*.jar")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-extensions-profile.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-backup.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-all-rules.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-test-rule.xml")) + .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-kotlin-profile.xml")) + .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-kotlin-all-rules.xml")) .build(); return new PmdTestOrchestrator(orchestrator); @@ -126,6 +138,10 @@ private static String determineJavaPluginVersion() { return System.getProperty(SONAR_JAVA_PLUGIN_VERSION_KEY, "LATEST_RELEASE[8.15]"); // use 8.9 to test with SQ 9.9 } + private static String determineKotlinPluginVersion() { + return System.getProperty(SONAR_KOTLIN_PLUGIN_VERSION_KEY, "LATEST_RELEASE[2.15]"); + } + private static String determineSonarqubeVersion() { return System.getProperty(SONAR_VERSION_KEY, "LATEST_RELEASE[25.6]"); // use SQ 9.9.4 to test with old version } diff --git a/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-all-rules.xml b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-all-rules.xml new file mode 100644 index 00000000..ea1391c6 --- /dev/null +++ b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-all-rules.xml @@ -0,0 +1,17 @@ + + + pmd-kotlin-all-rules + kotlin + + + pmd-kotlin + FunctionNameTooShort + MAJOR + + + pmd-kotlin + OverrideBothEqualsAndHashcode + MAJOR + + + diff --git a/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-profile.xml b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-profile.xml new file mode 100644 index 00000000..c50b5369 --- /dev/null +++ b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-kotlin-profile.xml @@ -0,0 +1,17 @@ + + + pmd-kotlin-profile + kotlin + + + pmd-kotlin + FunctionNameTooShort + CRITICAL + + + pmd-kotlin + OverrideBothEqualsAndHashcode + MAJOR + + + From fd0c9dbd43d9c2fe3cb036bac9d5aa93440d7fba Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Sun, 20 Jul 2025 14:17:43 +0200 Subject: [PATCH 04/10] fix build after merge --- .../sonar/plugins/pmd/PmdExecutorFactory.java | 93 ------------------- .../java/org/sonar/plugins/pmd/PmdPlugin.java | 1 - .../plugins/pmd/PmdExecutorFactoryTest.java | 62 ------------- .../org/sonar/plugins/pmd/PmdPluginTest.java | 3 +- 4 files changed, 1 insertion(+), 158 deletions(-) delete mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java delete mode 100644 sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java deleted file mode 100644 index 6c5ec8ce..00000000 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdExecutorFactory.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SonarQube PMD7 Plugin - * Copyright (C) 2012-2021 SonarSource SA and others - * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.plugins.pmd; - -import org.sonar.api.batch.ScannerSide; -import org.sonar.api.batch.fs.FileSystem; -import org.sonar.api.batch.rule.ActiveRules; -import org.sonar.api.config.Configuration; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.plugins.java.api.JavaResourceLocator; - -/** - * Factory for creating PmdExecutor instances. - * This class handles the case where JavaResourceLocator is not available - * (e.g., when analyzing non-Java projects). - */ -@ScannerSide -public class PmdExecutorFactory { - - private static final Logger LOGGER = Loggers.get(PmdExecutorFactory.class); - - private final FileSystem fileSystem; - private final ActiveRules activeRules; - private final PmdConfiguration pmdConfiguration; - private final Configuration settings; - private final JavaResourceLocator javaResourceLocator; - - /** - * Constructor with all dependencies including JavaResourceLocator. - * This constructor will be used by Sonar's dependency injection system - * when JavaResourceLocator is available. - */ - public PmdExecutorFactory(FileSystem fileSystem, ActiveRules activeRules, - PmdConfiguration pmdConfiguration, Configuration settings, - JavaResourceLocator javaResourceLocator) { - this.fileSystem = fileSystem; - this.activeRules = activeRules; - this.pmdConfiguration = pmdConfiguration; - this.settings = settings; - this.javaResourceLocator = javaResourceLocator; - LOGGER.debug("PmdExecutorFactory created with JavaResourceLocator"); - } - - /** - * Constructor without JavaResourceLocator. - * This constructor will be used by Sonar's dependency injection system - * when JavaResourceLocator is not available. - */ - public PmdExecutorFactory(FileSystem fileSystem, ActiveRules activeRules, - PmdConfiguration pmdConfiguration, Configuration settings) { - this.fileSystem = fileSystem; - this.activeRules = activeRules; - this.pmdConfiguration = pmdConfiguration; - this.settings = settings; - this.javaResourceLocator = null; - LOGGER.debug("PmdExecutorFactory created without JavaResourceLocator"); - } - - /** - * Creates a PmdExecutor instance. - * If JavaResourceLocator is available, it will be passed to the PmdExecutor. - * Otherwise, a PmdExecutor without JavaResourceLocator will be created. - * - * @return A PmdExecutor instance - */ - public PmdExecutor create() { - if (javaResourceLocator != null) { - LOGGER.debug("Creating PmdExecutor with JavaResourceLocator"); - return new PmdExecutor(fileSystem, activeRules, pmdConfiguration, javaResourceLocator, settings); - } else { - LOGGER.debug("Creating PmdExecutor without JavaResourceLocator"); - return new PmdExecutor(fileSystem, activeRules, pmdConfiguration, settings); - } - } -} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java index cfa06686..6fe1c39a 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java @@ -44,7 +44,6 @@ public void define(Context context) { PmdSensor.class, PmdConfiguration.class, - PmdExecutorFactory.class, PmdJavaExecutor.class, PmdKotlinExecutor.class, PmdRulesDefinition.class, diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java deleted file mode 100644 index ef70c30e..00000000 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdExecutorFactoryTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarQube PMD7 Plugin - * Copyright (C) 2012-2021 SonarSource SA and others - * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.plugins.pmd; - -import org.junit.jupiter.api.Test; -import org.sonar.api.batch.fs.FileSystem; -import org.sonar.api.batch.rule.ActiveRules; -import org.sonar.api.config.Configuration; -import org.sonar.plugins.java.api.JavaResourceLocator; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -class PmdExecutorFactoryTest { - - private final FileSystem fs = mock(FileSystem.class); - private final ActiveRules activeRules = mock(ActiveRules.class); - private final PmdConfiguration pmdConfiguration = mock(PmdConfiguration.class); - private final Configuration settings = mock(Configuration.class); - private final JavaResourceLocator javaResourceLocator = mock(JavaResourceLocator.class); - - @Test - void should_create_executor_with_java_resource_locator_when_available() { - // given - PmdExecutorFactory factory = new PmdExecutorFactory(fs, activeRules, pmdConfiguration, settings, javaResourceLocator); - - // when - PmdExecutor executor = factory.create(); - - // then - assertThat(executor).isNotNull(); - } - - @Test - void should_create_executor_without_java_resource_locator_when_not_available() { - // given - PmdExecutorFactory factory = new PmdExecutorFactory(fs, activeRules, pmdConfiguration, settings); - - // when - PmdExecutor executor = factory.create(); - - // then - assertThat(executor).isNotNull(); - } -} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java index cdc55806..e63331e7 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java @@ -54,11 +54,10 @@ void testPluginConfiguration() { // then final List extensions = context.getExtensions(); assertThat(extensions) - .hasSize(12) + .hasSize(13) .contains( PmdSensor.class, PmdConfiguration.class, - PmdExecutorFactory.class, PmdJavaExecutor.class, PmdKotlinExecutor.class, PmdRulesDefinition.class, From 70c0b17041769c929fec9f725f8638fae6067539 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Sun, 20 Jul 2025 14:33:59 +0200 Subject: [PATCH 05/10] added Apex executor --- sonar-pmd-plugin/pom.xml | 4 +- .../sonar/plugins/pmd/PmdApexExecutor.java | 87 +++++++++++++ .../java/org/sonar/plugins/pmd/PmdPlugin.java | 1 + .../plugins/pmd/PmdApexExecutorTest.java | 121 ++++++++++++++++++ .../org/sonar/plugins/pmd/PmdPluginTest.java | 3 +- .../org/sonar/plugins/pmd/simple-apex.xml | 7 + 6 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java create mode 100644 sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java create mode 100644 sonar-pmd-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml diff --git a/sonar-pmd-plugin/pom.xml b/sonar-pmd-plugin/pom.xml index 15b2a0bc..080b915b 100644 --- a/sonar-pmd-plugin/pom.xml +++ b/sonar-pmd-plugin/pom.xml @@ -186,8 +186,8 @@ pmd PMD org.sonar.plugins.pmd.PmdPlugin - Analyze Java and Kotlin code with PMD. - java,kotlin + Analyze Java, Kotlin, and Apex code with PMD. + java,kotlin,apex diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java new file mode 100644 index 00000000..3e7e57ff --- /dev/null +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java @@ -0,0 +1,87 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd; + +import net.sourceforge.pmd.reporting.FileAnalysisListener; +import net.sourceforge.pmd.reporting.Report; +import org.sonar.api.batch.ScannerSide; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile.Type; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.config.Configuration; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * PMD executor for Apex files. + */ +@ScannerSide +public class PmdApexExecutor extends AbstractPmdExecutor { + + public PmdApexExecutor(FileSystem fileSystem, ActiveRules rulesProfile, + PmdConfiguration pmdConfiguration, Configuration settings) { + super(fileSystem, rulesProfile, pmdConfiguration, settings); + } + + @Override + protected String getStartMessage() { + return "Execute PMD {} for Apex"; + } + + @Override + protected String getEndMessage() { + return "Execute PMD {} for Apex (done) | time={}ms"; + } + + @Override + protected Report executePmd(URLClassLoader classLoader) { + final PmdTemplate pmdFactory = createPmdTemplate(classLoader); + final Optional apexMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); + final Optional apexTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_APEX_KEY), PmdConstants.MAIN_APEX_REPOSITORY_KEY); + + if (LOGGER.isDebugEnabled()) { + apexMainReport.ifPresent(this::writeDebugLine); + apexTestReport.ifPresent(this::writeDebugLine); + } + + Consumer fileAnalysisListenerConsumer = AbstractPmdExecutor::accept; + + Report unionReport = Report.buildReport(fileAnalysisListenerConsumer); + unionReport = apexMainReport.map(unionReport::union).orElse(unionReport); + unionReport = apexTestReport.map(unionReport::union).orElse(unionReport); + + pmdConfiguration.dumpXmlReport(unionReport); + + return unionReport; + } + + /** + * @return A classloader for PMD that contains no additional dependencies. + * For Apex projects, we don't need the project's classpath. + */ + @Override + protected URLClassLoader createClassloader() { + // Create an empty URLClassLoader + return new URLClassLoader(new URL[0]); + } +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java index 6fe1c39a..282679f5 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPlugin.java @@ -46,6 +46,7 @@ public void define(Context context) { PmdConfiguration.class, PmdJavaExecutor.class, PmdKotlinExecutor.class, + PmdApexExecutor.class, PmdRulesDefinition.class, PmdKotlinRulesDefinition.class, PmdApexRulesDefinition.class, diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java new file mode 100644 index 00000000..f2c22634 --- /dev/null +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java @@ -0,0 +1,121 @@ +/* + * SonarQube PMD7 Plugin + * Copyright (C) 2012-2021 SonarSource SA and others + * mailto:jborgers AT jpinpoint DOT com; peter.paul.bakker AT stokpop DOT nl + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.pmd; + +import net.sourceforge.pmd.lang.rule.RuleSet; +import net.sourceforge.pmd.reporting.Report; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.sonar.api.batch.fs.InputFile.Type; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; + +import java.net.URLClassLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.Mockito.*; + +class PmdApexExecutorTest extends AbstractPmdExecutorTest { + + private PmdApexExecutor realPmdExecutor; + + @BeforeEach + void setUp() { + realPmdExecutor = new PmdApexExecutor( + fileSystem, + activeRules, + pmdConfiguration, + settings.asConfig() + ); + pmdExecutor = Mockito.spy(realPmdExecutor); + } + + protected static DefaultInputFile fileApex(String path, Type type) { + return TestInputFileBuilder.create("", path) + .setType(type) + .setLanguage(PmdConstants.LANGUAGE_APEX_KEY) + .build(); + } + + @Override + protected DefaultInputFile getAppropriateInputFileForTest() { + return fileApex("src/test/apex/TestApex.cls", Type.MAIN); + } + + @Test + void should_execute_pmd_on_apex_source_files() { + // Given + DefaultInputFile srcFile = fileApex("src/test/apex/TestApex.cls", Type.MAIN); + setupPmdRuleSet(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "simple-apex.xml"); + fileSystem.add(srcFile); + + // When + Report report = pmdExecutor.execute(); + + // Then + assertThat(report).isNotNull(); + verify(pmdConfiguration).dumpXmlReport(report); + } + + @Test + void should_execute_pmd_on_apex_test_files() { + // Given + DefaultInputFile testFile = fileApex("src/test/apex/TestApexTest.cls", Type.TEST); + setupPmdRuleSet(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "simple-apex.xml"); + fileSystem.add(testFile); + + // When + Report report = pmdExecutor.execute(); + + // Then + assertThat(report).isNotNull(); + verify(pmdConfiguration).dumpXmlReport(report); + } + + @Test + void should_ignore_empty_apex_test_dir() { + // Given + DefaultInputFile srcFile = fileApex("src/test/apex/TestApex.cls", Type.MAIN); + doReturn(pmdTemplate).when(pmdExecutor).createPmdTemplate(any(URLClassLoader.class)); + setupPmdRuleSet(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "simple-apex.xml"); + fileSystem.add(srcFile); + + // When + pmdExecutor.execute(); + + // Then + verify(pmdTemplate).process(anyIterable(), any(RuleSet.class)); + verifyNoMoreInteractions(pmdTemplate); + } + + @Test + void should_create_empty_classloader() throws Exception { + // When + pmdExecutor.execute(); + + // Then + // Verify that createPmdTemplate is called with a URLClassLoader that has no URLs + verify(pmdExecutor).createPmdTemplate(argThat(classLoader -> + classLoader instanceof URLClassLoader && ((URLClassLoader) classLoader).getURLs().length == 0)); + } +} \ No newline at end of file diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java index e63331e7..49d926eb 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdPluginTest.java @@ -54,12 +54,13 @@ void testPluginConfiguration() { // then final List extensions = context.getExtensions(); assertThat(extensions) - .hasSize(13) + .hasSize(14) .contains( PmdSensor.class, PmdConfiguration.class, PmdJavaExecutor.class, PmdKotlinExecutor.class, + PmdApexExecutor.class, PmdRulesDefinition.class, PmdKotlinRulesDefinition.class, PmdApexRulesDefinition.class, diff --git a/sonar-pmd-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml b/sonar-pmd-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml new file mode 100644 index 00000000..a48fc460 --- /dev/null +++ b/sonar-pmd-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml @@ -0,0 +1,7 @@ + + + Sonar PMD apex rules + + 2 + + From 7e342495714c9bff27033a82f273c39c02e3510e Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Sun, 20 Jul 2025 14:51:25 +0200 Subject: [PATCH 06/10] added Apex Sensor detection --- .../java/org/sonar/plugins/pmd/PmdSensor.java | 18 ++- .../plugins/pmd/languages/ApexLanguage.java | 4 +- .../org/sonar/plugins/pmd/PmdSensorTest.java | 118 +++++++++++++++++- 3 files changed, 136 insertions(+), 4 deletions(-) diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java index 9c33042d..287b5fc1 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdSensor.java @@ -32,14 +32,16 @@ public class PmdSensor implements Sensor { private final ActiveRules profile; private final PmdJavaExecutor javaExecutor; private final PmdKotlinExecutor kotlinExecutor; + private final PmdApexExecutor apexExecutor; private final PmdViolationRecorder pmdViolationRecorder; private final FileSystem fs; public PmdSensor(ActiveRules profile, PmdJavaExecutor javaExecutor, PmdKotlinExecutor kotlinExecutor, - PmdViolationRecorder pmdViolationRecorder, FileSystem fs) { + PmdApexExecutor apexExecutor, PmdViolationRecorder pmdViolationRecorder, FileSystem fs) { this.profile = profile; this.javaExecutor = javaExecutor; this.kotlinExecutor = kotlinExecutor; + this.apexExecutor = apexExecutor; this.pmdViolationRecorder = pmdViolationRecorder; this.fs = fs; } @@ -84,6 +86,10 @@ public void execute(SensorContext context) { boolean hasJavaFiles = hasFilesToCheck(Type.MAIN, PmdConstants.MAIN_JAVA_REPOSITORY_KEY, PmdConstants.LANGUAGE_JAVA_KEY) || hasFilesToCheck(Type.TEST, PmdConstants.MAIN_JAVA_REPOSITORY_KEY, PmdConstants.LANGUAGE_JAVA_KEY); + // Check if there are Apex files to analyze + boolean hasApexFiles = hasFilesToCheck(Type.MAIN, PmdConstants.MAIN_APEX_REPOSITORY_KEY, PmdConstants.LANGUAGE_APEX_KEY) || + hasFilesToCheck(Type.TEST, PmdConstants.MAIN_APEX_REPOSITORY_KEY, PmdConstants.LANGUAGE_APEX_KEY); + // Process Kotlin files if present if (hasKotlinFiles) { net.sourceforge.pmd.reporting.Report kotlinReport = kotlinExecutor.execute(); @@ -103,6 +109,16 @@ public void execute(SensorContext context) { } } } + + // Process Apex files if present + if (hasApexFiles) { + net.sourceforge.pmd.reporting.Report apexReport = apexExecutor.execute(); + if (apexReport != null) { + for (RuleViolation violation : apexReport.getViolations()) { + pmdViolationRecorder.saveViolation(violation, context); + } + } + } } } } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java index 0a4d13fb..4b2e2ff2 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java @@ -21,11 +21,13 @@ import org.sonar.api.config.Configuration; import org.sonar.api.resources.AbstractLanguage; +import org.sonar.api.server.ServerSide; import org.sonar.plugins.pmd.PmdConstants; /** * This class defines the Apex language. */ +@ServerSide public final class ApexLanguage extends AbstractLanguage { private final Configuration config; @@ -47,4 +49,4 @@ public String[] getFileSuffixes() { } return suffixes; } -} \ No newline at end of file +} diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java index 8ae9a801..005e580c 100644 --- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java +++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdSensorTest.java @@ -44,6 +44,7 @@ class PmdSensorTest { private final ActiveRules profile = mock(ActiveRules.class, RETURNS_DEEP_STUBS); private final PmdJavaExecutor javaExecutor = mock(PmdJavaExecutor.class); private final PmdKotlinExecutor kotlinExecutor = mock(PmdKotlinExecutor.class); + private final PmdApexExecutor apexExecutor = mock(PmdApexExecutor.class); private final PmdViolationRecorder pmdViolationRecorder = mock(PmdViolationRecorder.class); private final SensorContext sensorContext = mock(SensorContext.class); private final DefaultFileSystem fs = new DefaultFileSystem(new File(".")); @@ -52,7 +53,7 @@ class PmdSensorTest { @BeforeEach void setUpPmdSensor() { - pmdSensor = new PmdSensor(profile, javaExecutor, kotlinExecutor, pmdViolationRecorder, fs); + pmdSensor = new PmdSensor(profile, javaExecutor, kotlinExecutor, apexExecutor, pmdViolationRecorder, fs); } @Test @@ -119,6 +120,7 @@ void should_not_execute_on_project_without_any_files() { // then verify(javaExecutor, never()).execute(); verify(kotlinExecutor, never()).execute(); + verify(apexExecutor, never()).execute(); } @Test @@ -129,9 +131,12 @@ void should_not_execute_on_project_without_active_rules() { addOneJavaFile(Type.TEST); addOneKotlinFile(Type.MAIN); addOneKotlinFile(Type.TEST); + addOneApexFile(Type.MAIN); + addOneApexFile(Type.TEST); when(profile.findByRepository(PmdConstants.MAIN_JAVA_REPOSITORY_KEY).isEmpty()).thenReturn(true); when(profile.findByRepository(PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY).isEmpty()).thenReturn(true); + when(profile.findByRepository(PmdConstants.MAIN_APEX_REPOSITORY_KEY).isEmpty()).thenReturn(true); // when pmdSensor.execute(sensorContext); @@ -139,6 +144,7 @@ void should_not_execute_on_project_without_active_rules() { // then verify(javaExecutor, never()).execute(); verify(kotlinExecutor, never()).execute(); + verify(apexExecutor, never()).execute(); } @Test @@ -265,6 +271,95 @@ void pmdSensorShouldNotRethrowKotlinExecutorExceptions() { .isEqualTo(expectedException); } + @Test + void should_execute_apex_executor_on_project_with_apex_main_files() { + + // given + addOneApexFile(Type.MAIN); + + // when + pmdSensor.execute(sensorContext); + + // then + verify(apexExecutor, atLeastOnce()).execute(); + } + + @Test + void should_execute_apex_executor_on_project_with_apex_test_files() { + + // given + addOneApexFile(Type.TEST); + + // when + pmdSensor.execute(sensorContext); + + // then + verify(apexExecutor, atLeastOnce()).execute(); + } + + @Test + void should_report_apex_violations() { + + // given + addOneApexFile(Type.MAIN); + final RuleViolation pmdViolation = violation(); + mockApexExecutorResult(pmdViolation); + + // when + pmdSensor.execute(sensorContext); + + // then + verify(pmdViolationRecorder).saveViolation(pmdViolation, sensorContext); + } + + @Test + void should_not_report_zero_apex_violation() { + + // given + mockApexExecutorResult(); + addOneApexFile(Type.MAIN); + + // when + pmdSensor.execute(sensorContext); + + // then + verify(pmdViolationRecorder, never()).saveViolation(any(RuleViolation.class), eq(sensorContext)); + verifyNoMoreInteractions(sensorContext); + } + + @Test + void should_not_report_invalid_apex_violation() { + + // given + mockApexExecutorResult(violation()); + addOneApexFile(Type.MAIN); + + // when + pmdSensor.execute(sensorContext); + + // then + verify(pmdViolationRecorder, never()).saveViolation(any(RuleViolation.class), eq(sensorContext)); + verifyNoMoreInteractions(sensorContext); + } + + @Test + void pmdSensorShouldNotRethrowApexExecutorExceptions() { + + // given + addOneApexFile(Type.MAIN); + + final RuntimeException expectedException = new RuntimeException(); + when(apexExecutor.execute()).thenThrow(expectedException); + + // when + final Throwable thrown = catchThrowable(() -> pmdSensor.execute(sensorContext)); + + // then + assertThat(thrown) + .isInstanceOf(RuntimeException.class) + .isEqualTo(expectedException); + } + @Test void should_to_string() { final String toString = pmdSensor.toString(); @@ -300,6 +395,11 @@ private void mockKotlinExecutorResult(RuleViolation... violations) { .thenReturn(createReport(violations)); } + private void mockApexExecutorResult(RuleViolation... violations) { + when(apexExecutor.execute()) + .thenReturn(createReport(violations)); + } + private Report createReport(RuleViolation... violations) { Consumer fileAnalysisListenerConsumer = fal -> { for (RuleViolation violation : violations) { @@ -337,4 +437,18 @@ private void addOneKotlinFile(Type type) { .build() ); } -} \ No newline at end of file + + private void addOneApexFile(Type type) { + mockApexExecutorResult(); + File file = new File("x.cls"); + fs.add( + TestInputFileBuilder.create( + "sonar-pmd-test", + file.getName() + ) + .setLanguage("apex") + .setType(type) + .build() + ); + } +} From 344036369e7279d1a347f4bd9fb3d42263f13a36 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Sun, 20 Jul 2025 15:35:42 +0200 Subject: [PATCH 07/10] fix apex integration tests --- integration-test/pom.xml | 13 +------------ .../suite/orchestrator/PmdTestOrchestrator.java | 2 +- sonar-pmd-plugin/pom.xml | 2 +- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/integration-test/pom.xml b/integration-test/pom.xml index dc5d5d00..288f7f30 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -30,7 +30,7 @@ integration-test SonarQube PMD Plugin Integration Test - sonar-plugin + jar 2013 @@ -96,17 +96,6 @@ org.sonarsource.scanner.maven sonar-maven-plugin - - org.sonarsource.sonar-packaging-maven-plugin - sonar-packaging-maven-plugin - true - - org.sonar.examples.pmd.PmdExtensionPlugin - Integration Test PmdExtensionPlugin - - pmd - - maven-clean-plugin diff --git a/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java b/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java index 9a06d37c..ac6f5597 100644 --- a/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java +++ b/integration-test/src/test/java/com/sonar/it/java/suite/orchestrator/PmdTestOrchestrator.java @@ -113,13 +113,13 @@ public static PmdTestOrchestrator init() { determineKotlinPluginVersion() )) .addPlugin(byWildcardMavenFilename(new File("../sonar-pmd-plugin/target"), "sonar-pmd-plugin-*.jar")) - .addPlugin(byWildcardMavenFilename(new File("./target"), "integration-test-*.jar")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-extensions-profile.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-backup.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-all-rules.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-test-rule.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-kotlin-profile.xml")) .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-kotlin-all-rules.xml")) + .restoreProfileAtStartup(ofClasspath("/com/sonar/it/java/PmdTest/pmd-apex-profile.xml")) .build(); return new PmdTestOrchestrator(orchestrator); diff --git a/sonar-pmd-plugin/pom.xml b/sonar-pmd-plugin/pom.xml index 080b915b..953b7cab 100644 --- a/sonar-pmd-plugin/pom.xml +++ b/sonar-pmd-plugin/pom.xml @@ -203,7 +203,7 @@ - 20000000 + 60000000 12000000 ${project.build.directory}/${project.build.finalName}.jar From 7362b623fd0739ed76edb174974f66278d99613a Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Sun, 20 Jul 2025 15:48:42 +0200 Subject: [PATCH 08/10] language is named "Apex" instead of "PMD Apex" --- .../src/main/java/org/sonar/plugins/pmd/PmdConstants.java | 1 + .../main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java index 8b13294e..2bc1257a 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java @@ -31,6 +31,7 @@ public final class PmdConstants { public static final String REPOSITORY_NAME = "PMD"; public static final String REPOSITORY_KOTLIN_NAME = "PMD Kotlin"; public static final String REPOSITORY_APEX_NAME = "PMD Apex"; + public static final String LANGUAGE_APEX_NAME = "Apex"; public static final String XPATH_CLASS = "net.sourceforge.pmd.lang.rule.xpath.XPathRule"; public static final String XPATH_EXPRESSION_PARAM = "xpath"; public static final String XPATH_MESSAGE_PARAM = "message"; diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java index 4b2e2ff2..ba5492c1 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java +++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java @@ -37,7 +37,7 @@ public final class ApexLanguage extends AbstractLanguage { * @param config The SonarQube configuration */ public ApexLanguage(Configuration config) { - super(PmdConstants.LANGUAGE_APEX_KEY, PmdConstants.REPOSITORY_APEX_NAME); + super(PmdConstants.LANGUAGE_APEX_KEY, PmdConstants.LANGUAGE_APEX_NAME); this.config = config; } From d14f096ddaa28f79f7cf3419bd7dfc52c69c4b78 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Sun, 20 Jul 2025 15:57:31 +0200 Subject: [PATCH 09/10] small jar size is ok! --- sonar-pmd-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-pmd-plugin/pom.xml b/sonar-pmd-plugin/pom.xml index 953b7cab..080b915b 100644 --- a/sonar-pmd-plugin/pom.xml +++ b/sonar-pmd-plugin/pom.xml @@ -203,7 +203,7 @@ - 60000000 + 20000000 12000000 ${project.build.directory}/${project.build.finalName}.jar From 716fe25e54acbeef83e40e2aaa0c4a821180c975 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 21 Jul 2025 09:27:08 +0200 Subject: [PATCH 10/10] update generated rules-apex.xml --- .../org/sonar/plugins/pmd/rules-apex.xml | 482 ++++++++++-------- .../org/sonar/plugins/pmd/rules-java.xml | 2 +- 2 files changed, 275 insertions(+), 209 deletions(-) diff --git a/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml b/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml index cc32ff19..1e7545cc 100644 --- a/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml +++ b/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml @@ -5,11 +5,12 @@ Apex assertions should include message category/apex/bestpractices.xml/ApexAssertionsShouldIncludeMessage MAJOR - The second parameter of System.assert/third parameter of System.assertEquals/System.assertNotEquals is a message. + Title of issues: Apex test assert statement should make use of the message parameter. +

The second parameter of System.assert/third parameter of System.assertEquals/System.assertNotEquals is a message. Having a second/third parameter provides more information and makes it easier to debug the test failure and improves the readability of test output.

Example

-

 @isTest
+

 @isTest
  public class Foo {
      @isTest
      static void methodATest() {
@@ -19,7 +20,7 @@ improves the readability of test output.

System.assert(o.isClosed, 'Opportunity is not closed.'); // good } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -28,25 +29,27 @@ improves the readability of test output.

Apex bad crypto category/apex/security.xml/ApexBadCrypto MAJOR - The rule makes sure you are using randomly generated IVs and keys for Crypto calls. + Title of issues: Apex classes should use random IV/key +

The rule makes sure you are using randomly generated IVs and keys for Crypto calls. Hard-wiring these values greatly compromises the security of encrypted data.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      Blob hardCodedIV = Blob.valueOf('Hardcoded IV 123');
      Blob hardCodedKey = Blob.valueOf('0000000000000000');
      Blob data = Blob.valueOf('Data to be encrypted');
      Blob encrypted = Crypto.encrypt('AES128', hardCodedKey, hardCodedIV, data);
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security ApexCRUDViolation - Apex c r u d violation + Apex CRUDViolation category/apex/security.xml/ApexCRUDViolation MAJOR - The rule validates you are checking for access permissions before a SOQL/SOSL/DML operation. + Title of issues: Validate CRUD permission before SOQL/DML operation or enforce user mode +

The rule validates you are checking for access permissions before a SOQL/SOSL/DML operation. Since Apex runs by default in system mode not having proper permissions checks results in escalation of privilege and may produce runtime errors. This check forces you to handle such scenarios.

Since Winter '23 (API Version 56) you can enforce user mode for database operations by using @@ -78,7 +81,7 @@ check happens automatically and is not needed explicitly. However, the rule can' whether a getter is a VF getter or not and reports a violation in any case. In such cases, the violation should be suppressed.

Example

-

 public class Foo {
+

 public class Foo {
      public Contact foo(String status, String ID) {
  
          // validate you can actually query what you intend to retrieve
@@ -94,16 +97,17 @@ should be suppressed.

return c; } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security ApexCSRF - Apex c s r f + Apex CSRF category/apex/errorprone.xml/ApexCSRF MAJOR - Having DML operations in Apex class constructor or initializers can have unexpected side effects: + Title of issues: Avoid making DML operations in Apex class constructor or initializers +

Having DML operations in Apex class constructor or initializers can have unexpected side effects: By just accessing a page, the DML statements would be executed and the database would be modified. Just querying the database is permitted.

In addition to constructors and initializers, any method called init is checked as well.

@@ -111,7 +115,7 @@ should be suppressed.

Note: This rule has been moved from category "Security" to "Error Prone" with PMD 6.21.0, since using DML in constructors is not a security problem, but crashes the application.

Example

-

 public class Foo {
+

 public class Foo {
      // initializer
      {
          insert data;
@@ -127,7 +131,7 @@ should be suppressed.

insert data; } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -136,16 +140,17 @@ should be suppressed.

Apex dangerous methods category/apex/security.xml/ApexDangerousMethods MAJOR - Checks against calling dangerous methods.

+ Title of issues: Calling potentially dangerous method +

Checks against calling dangerous methods.

For the time being, it reports:

  • Against FinancialForce's Configuration.disableTriggerCRUDSecurity(). Disabling CRUD security opens the door to several attacks and requires manual validation, which is unreliable.
  • Calling System.debug passing sensitive data as parameter, which could lead to exposure of private data.

Example

-

 public class Foo {
+

 public class Foo {
      public Foo() {
          Configuration.disableTriggerCRUDSecurity();
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security @@ -154,11 +159,12 @@ should be suppressed.

Apex doc category/apex/documentation.xml/ApexDoc MAJOR - This rule validates that:

+ Title of issues: ApexDoc comment is missing or incorrect +

This rule validates that:

  • ApexDoc comments are present for classes, methods, and properties that are public or global, excluding overrides and test classes (as well as the contents of test classes).
  • ApexDoc comments are present for classes, methods, and properties that are protected or private, depending on the properties reportPrivate and reportProtected.
  • ApexDoc comments should contain @description depending on the property reportMissingDescription.
  • ApexDoc comments on non-void, non-constructor methods should contain @return.
  • ApexDoc comments on void or constructor methods should not contain @return.
  • ApexDoc comments on methods with parameters should contain @param for each parameter, in the same order as the method signature.
  • ApexDoc comments are present on properties is only validated, if the property reportProperty is enabled. By setting reportProperty to false, you can ignore missing comments on properties.

Method overrides and tests are both exempted from having ApexDoc.

Example

-

 /**
+

 /**
   * @description Hello World
   */
  public class HelloWorld {
@@ -168,7 +174,7 @@ should be suppressed.

*/ public Object bar() { return null; } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd documentation @@ -177,16 +183,17 @@ should be suppressed.

Apex insecure endpoint category/apex/security.xml/ApexInsecureEndpoint MAJOR - Checks against accessing endpoints under plain http. You should always use + Title of issues: Apex callouts should use encrypted communication channels +

Checks against accessing endpoints under plain http. You should always use https for security.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      void foo() {
          HttpRequest req = new HttpRequest();
          req.setEndpoint('http://localhost:com');
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security @@ -195,32 +202,34 @@ should be suppressed.

Apex open redirect category/apex/security.xml/ApexOpenRedirect MAJOR - Checks against redirects to user-controlled locations. This prevents attackers from + Title of issues: Apex classes should safely redirect to a known location +

Checks against redirects to user-controlled locations. This prevents attackers from redirecting users to phishing sites.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      String unsafeLocation = ApexPage.getCurrentPage().getParameters.get('url_param');
      PageReference page() {
         return new PageReference(unsafeLocation);
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security ApexSOQLInjection - Apex s o q l injection + Apex SOQLInjection category/apex/security.xml/ApexSOQLInjection MAJOR - Detects the usage of untrusted / unescaped variables in DML queries.

+ Title of issues: Avoid untrusted/unescaped variables in DML query +

Detects the usage of untrusted / unescaped variables in DML queries.

Example

-

 public class Foo {
+

 public class Foo {
      public void test1(String t1) {
          Database.query('SELECT Id FROM Account' + t1);
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security @@ -229,13 +238,14 @@ redirecting users to phishing sites.

Apex sharing violations category/apex/security.xml/ApexSharingViolations MAJOR - Detect classes declared without explicit sharing mode if DML methods are used. This + Title of issues: Apex classes should declare a sharing model if DML or SOQL/SOSL is used +

Detect classes declared without explicit sharing mode if DML methods are used. This forces the developer to take access restrictions into account before modifying objects.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      // DML operation here
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security @@ -244,20 +254,21 @@ forces the developer to take access restrictions into account before modifying o Apex suggest using named cred category/apex/security.xml/ApexSuggestUsingNamedCred MAJOR - Detects hardcoded credentials used in requests to an endpoint.

+ Title of issues: Suggest named credentials for authentication +

Detects hardcoded credentials used in requests to an endpoint.

You should refrain from hardcoding credentials:

  • They are hard to mantain by being mixed in application code
  • Particularly hard to update them when used from different classes
  • Granting a developer access to the codebase means granting knowledge of credentials, keeping a two-level access is not possible.
  • Using different credentials for different environments is troublesome and error-prone.

Instead, you should use Named Credentials and a callout endpoint.

For more information, you can check this

Example

-

 public class Foo {
+

 public class Foo {
      public void foo(String username, String password) {
          Blob headerValue = Blob.valueOf(username + ':' + password);
          String authorizationHeader = 'BASIC ' + EncodingUtil.base64Encode(headerValue);
          req.setHeader('Authorization', authorizationHeader);
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security @@ -266,11 +277,12 @@ forces the developer to take access restrictions into account before modifying o Apex unit test class should have asserts category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveAsserts MAJOR - Apex unit tests should include at least one assertion. This makes the tests more robust, and using assert + Title of issues: Apex unit tests should System.assert() or assertEquals() or assertNotEquals() +

Apex unit tests should include at least one assertion. This makes the tests more robust, and using assert with messages provide the developer a clearer idea of what the test does. Custom assert method invocation patterns can be specified using the 'additionalAssertMethodPattern' property if required.

Example

-

 @isTest
+

 @isTest
  public class Foo {
      public static testMethod void testSomething() {
          Account a = null;
@@ -279,7 +291,7 @@ patterns can be specified using the 'additionalAssertMethodPattern' property if
          a.toString();
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -288,10 +300,11 @@ patterns can be specified using the 'additionalAssertMethodPattern' property if Apex unit test class should have run as category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveRunAs MAJOR - Apex unit tests should include at least one runAs method. This makes the tests more robust, and independent from the + Title of issues: Apex unit test classes should have at least one System.runAs() call +

Apex unit tests should include at least one runAs method. This makes the tests more robust, and independent from the user running it.

Example

-

 @isTest
+

 @isTest
  private class TestRunAs {
     public static testMethod void testRunAs() {
          // Setup test data
@@ -312,7 +325,7 @@ user running it.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -321,12 +334,13 @@ user running it.

Apex unit test method should have is test annotation category/apex/bestpractices.xml/ApexUnitTestMethodShouldHaveIsTestAnnotation MAJOR - Apex test methods should have @isTest annotation instead of the testMethod keyword, + Title of issues: Apex test methods should have @isTest annotation. +

Apex test methods should have @isTest annotation instead of the testMethod keyword, as testMethod is deprecated. Salesforce advices to use @isTest annotation for test classes and methods.

Example

-

 @isTest
+

 @isTest
  private class ATest {
      @isTest
      static void methodATest() {
@@ -342,7 +356,7 @@ annotation for test classes and methods.

private void fetchData() { } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -351,9 +365,10 @@ annotation for test classes and methods.

Apex unit test should not use see all data true category/apex/bestpractices.xml/ApexUnitTestShouldNotUseSeeAllDataTrue MAJOR - Apex unit tests should not use @isTest(seeAllData=true) because it opens up the existing database data for unexpected modification by tests.

+ Title of issues: Apex unit tests should not use @isTest(seeAllData = true) +

Apex unit tests should not use @isTest(seeAllData=true) because it opens up the existing database data for unexpected modification by tests.

Example

-

 @isTest(seeAllData = true)
+

 @isTest(seeAllData = true)
  public class Foo {
      public static testMethod void testSomething() {
          Account a = null;
@@ -362,39 +377,41 @@ annotation for test classes and methods.

a.toString(); } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices ApexXSSFromEscapeFalse - Apex x s s from escape false + Apex XSSFrom escape false category/apex/security.xml/ApexXSSFromEscapeFalse MAJOR - Reports on calls to addError with disabled escaping. The message passed to addError + Title of issues: Apex classes should escape Strings in error messages +

Reports on calls to addError with disabled escaping. The message passed to addError will be displayed directly to the user in the UI, making it prime ground for XSS attacks if unescaped.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      Trigger.new[0].addError(vulnerableHTMLGoesHere, false);
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security ApexXSSFromURLParam - Apex x s s from u r l param + Apex XSSFrom URLParam category/apex/security.xml/ApexXSSFromURLParam MAJOR - Makes sure that all values obtained from URL parameters are properly escaped / sanitized + Title of issues: Apex classes should escape/sanitize Strings obtained from URL parameters +

Makes sure that all values obtained from URL parameters are properly escaped / sanitized to avoid XSS attacks.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      String unescapedstring = ApexPage.getCurrentPage().getParameters.get('url_param');
      String usedLater = unescapedstring;
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd security @@ -403,7 +420,8 @@ to avoid XSS attacks.

Avoid boolean method parameters category/apex/design.xml/AvoidBooleanMethodParameters CRITICAL - Boolean parameters in a system's API can make method calls difficult to understand and + Title of issues: Avoid Boolean method parameters +

Boolean parameters in a system's API can make method calls difficult to understand and maintain. They often indicate that a method is doing more than one thing and could benefit from being split into separate methods with more descriptive names.

@@ -412,7 +430,7 @@ to avoid XSS attacks.

separate methods, or configuration objects.

Examples

Example 1

-

 // Violates the rule: Uses a Boolean parameter
+

 // Violates the rule: Uses a Boolean parameter
  public class MyClass {
    public static void doSomething(Boolean isSomething) {
      if (isSomething == true) {
@@ -434,12 +452,12 @@ to avoid XSS attacks.

} }

Example 2

-

 public void setFlag(Boolean strict) { ... } // violation
+

 public void setFlag(Boolean strict) { ... } // violation
  
  // compliant
  public void enableStrictChecking() { ... }
  public void disableStrictChecking() { ... }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -448,11 +466,12 @@ to avoid XSS attacks.

Avoid debug statements category/apex/performance.xml/AvoidDebugStatements MAJOR - Debug statements contribute to longer transactions and consume Apex CPU time even when debug logs are not being captured.

+ Title of issues: Avoid debug statements since they impact on performance +

Debug statements contribute to longer transactions and consume Apex CPU time even when debug logs are not being captured.

When possible make use of other debugging techniques such as the Apex Replay Debugger and Checkpoints that could cover most use cases.

For other valid use cases that the statement is in fact valid make use of the @SuppressWarnings annotation or the //NOPMD comment.

Example

-

 public class Foo {
+

 public class Foo {
      public void bar() {
          Account acc = [SELECT Name, Owner.Name FROM Account LIMIT 1];
          System.debug(accs); // will get reported
@@ -467,7 +486,7 @@ to avoid XSS attacks.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd performance @@ -476,9 +495,10 @@ to avoid XSS attacks.

Avoid deeply nested if stmts category/apex/design.xml/AvoidDeeplyNestedIfStmts MAJOR - Avoid creating deeply nested if-then statements since they are harder to read and error-prone to maintain.

+ Title of issues: Deeply nested if..then statements are hard to read +

Avoid creating deeply nested if-then statements since they are harder to read and error-prone to maintain.

Example

-

 public class Foo {
+

 public class Foo {
      public void bar(Integer x, Integer y, Integer z) {
          if (x>y) {
              if (y>z) {
@@ -489,7 +509,7 @@ to avoid XSS attacks.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -498,16 +518,17 @@ to avoid XSS attacks.

Avoid direct access trigger map category/apex/errorprone.xml/AvoidDirectAccessTriggerMap MAJOR - Avoid directly accessing Trigger.old and Trigger.new as it can lead to a bug. Triggers should be bulkified and iterate through the map to handle the actions for each item separately.

+ Title of issues: Avoid directly accessing Trigger.old and Trigger.new +

Avoid directly accessing Trigger.old and Trigger.new as it can lead to a bug. Triggers should be bulkified and iterate through the map to handle the actions for each item separately.

Example

-

 trigger AccountTrigger on Account (before insert, before update) {
+

 trigger AccountTrigger on Account (before insert, before update) {
     Account a = Trigger.new[0]; //Bad: Accessing the trigger array directly is not recommended.
  
     for ( Account a : Trigger.new ) {
          //Good: Iterate through the trigger.new array instead.
     }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -516,15 +537,16 @@ to avoid XSS attacks.

Avoid global modifier category/apex/bestpractices.xml/AvoidGlobalModifier MAJOR - Global classes should be avoided (especially in managed packages) as they can never be deleted or changed in signature. Always check twice if something needs to be global. + Title of issues: Avoid using global modifier +

Global classes should be avoided (especially in managed packages) as they can never be deleted or changed in signature. Always check twice if something needs to be global. Many interfaces (e.g. Batch) required global modifiers in the past but don't require this anymore. Don't lock yourself in.

Example

-

 global class Unchangeable {
+

 global class Unchangeable {
      global UndeletableType unchangable(UndeletableType param) {
          // ...
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -533,11 +555,12 @@ Many interfaces (e.g. Batch) required global modifiers in the past but don't req Avoid hardcoding id category/apex/errorprone.xml/AvoidHardcodingId MAJOR - When deploying Apex code between sandbox and production environments, or installing Force.com AppExchange packages, + Title of issues: Hardcoding Id's is bound to break when changing environments. +

When deploying Apex code between sandbox and production environments, or installing Force.com AppExchange packages, it is essential to avoid hardcoding IDs in the Apex code. By doing so, if the record IDs change between environments, the logic can dynamically identify the proper data to operate against and not fail.

Example

-

 public without sharing class Foo {
+

 public without sharing class Foo {
      void foo() {
          //Error - hardcoded the record type id
          if (a.RecordTypeId == '012500000009WAr') {
@@ -547,7 +570,7 @@ Many interfaces (e.g. Batch) required global modifiers in the past but don't req
          }
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -556,11 +579,12 @@ Many interfaces (e.g. Batch) required global modifiers in the past but don't req Avoid logic in trigger category/apex/bestpractices.xml/AvoidLogicInTrigger MAJOR - As triggers do not allow methods like regular classes they are less flexible and suited to apply good encapsulation style. + Title of issues: Avoid logic in triggers +

As triggers do not allow methods like regular classes they are less flexible and suited to apply good encapsulation style. Therefore delegate the triggers work to a regular class (often called Trigger handler class).

See more here: https://developer.salesforce.com/page/Trigger_Frameworks_and_Apex_Trigger_Best_Practices

Example

-

 trigger Accounts on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
+

 trigger Accounts on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
      for(Account acc : Trigger.new) {
          if(Trigger.isInsert) {
              // ...
@@ -573,7 +597,7 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha
          }
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -582,17 +606,18 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha Avoid non existent annotations category/apex/errorprone.xml/AvoidNonExistentAnnotations MAJOR - Apex supported non existent annotations for legacy reasons. + Title of issues: Use of non existent annotations will lead to broken Apex code which will not compile in the future. +

Apex supported non existent annotations for legacy reasons. In the future, use of such non-existent annotations could result in broken apex code that will not compile. This will prevent users of garbage annotations from being able to use legitimate annotations added to Apex in the future. A full list of supported annotations can be found at https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation.htm

Example

-

 @NonExistentAnnotation public class ClassWithNonexistentAnnotation {
+

 @NonExistentAnnotation public class ClassWithNonexistentAnnotation {
      @NonExistentAnnotation public void methodWithNonExistentAnnotation() {
          // ...
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -601,11 +626,12 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha Avoid non restrictive queries category/apex/performance.xml/AvoidNonRestrictiveQueries MAJOR - When working with very large amounts of data, unfiltered SOQL or SOSL queries can quickly cause + Title of issues: Avoid {0} queries without a where or limit statement +

When working with very large amounts of data, unfiltered SOQL or SOSL queries can quickly cause governor limit exceptions.

Example

-

 public class Something {
+

 public class Something {
      public static void main( String[] as ) {
          Account[] accs1 = [ select id from account ];  // Bad
          Account[] accs2 = [ select id from account limit 10 ];  // better
@@ -613,7 +639,7 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha
          List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead]; // bad
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd performance @@ -622,14 +648,15 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha Avoid stateful database result category/apex/errorprone.xml/AvoidStatefulDatabaseResult CRITICAL - Using instance variables of the following types (or collections of these types) within a stateful batch class can cause serialization errors between batch iterations:

+ Title of issues: Using stateful Database.[x]Result instance variables can cause serialization errors between successive batch iterations. +

Using instance variables of the following types (or collections of these types) within a stateful batch class can cause serialization errors between batch iterations:

  • Database.DeleteResult
  • Database.EmptyRecycleBinResult
  • Database.MergeResult
  • Database.SaveResult
  • Database.UndeleteResult
  • Database.UpsertResult

This error occurs inconsistently and asynchronously with an obscure error message - making it particularly challenging to troubleshoot. See this issue for more details.

These errors can be avoided by marking the variable as static, transient, or using a different data type that is safe to serialize.

Example

-

 // Violating
+

 // Violating
  public class Example implements Database.Batchable<SObject>, Database.Stateful {
    List<Database.SaveResult> results = new List<Database.SaveResult>(); // This can cause failures
  
@@ -689,7 +716,7 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha
      return errors;
    }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -698,16 +725,17 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha Class naming conventions category/apex/codestyle.xml/ClassNamingConventions BLOCKER - Configurable naming conventions for type declarations. This rule reports + Title of issues: The {0} name '{1}' doesn't match '{2}' +

Configurable naming conventions for type declarations. This rule reports type declarations which do not match the regex that applies to their specific kind (e.g. enum or interface). Each regex can be configured through properties.

By default this rule uses the standard Apex naming convention (Pascal case).

Example

-

 public class FooClass { } // This is in pascal case, so it's ok
+

 public class FooClass { } // This is in pascal case, so it's ok
  
  public class fooClass { } // This will be reported unless you change the regex

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -716,7 +744,8 @@ Therefore delegate the triggers work to a regular class (often called Trigger ha Cognitive complexity category/apex/design.xml/CognitiveComplexity MAJOR - Methods that are highly complex are difficult to read and more costly to maintain. If you include too much decisional + Title of issues: The {0} '{1}' has a{2} cognitive complexity of {3}, current threshold is {4} +

Methods that are highly complex are difficult to read and more costly to maintain. If you include too much decisional logic within a single method, you make its behavior hard to understand and more difficult to modify.

Cognitive complexity is a measure of how difficult it is for humans to read and understand a method. Code that contains a break in the control flow is more complex, whereas the use of language shorthands doesn't increase the level of @@ -727,7 +756,7 @@ control flow leading to an increase in cognitive complexity.

By default, this rule reports methods with a complexity of 15 or more. Reported methods should be broken down into less complex components.

Example

-

 public class Foo {
+

 public class Foo {
      // Has a cognitive complexity of 0
      public void createAccount() {
          Account account = new Account(Name = 'PMD');
@@ -762,7 +791,7 @@ complex components.

update contactsToUpdate; } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -771,7 +800,8 @@ complex components.

Cyclomatic complexity category/apex/design.xml/CyclomaticComplexity MAJOR - The complexity of methods directly affects maintenance costs and readability. Concentrating too much decisional logic + Title of issues: The {0} '{1}' has a{2} cyclomatic complexity of {3}. +

The complexity of methods directly affects maintenance costs and readability. Concentrating too much decisional logic in a single method makes its behaviour hard to read and change.

Cyclomatic complexity assesses the complexity of a method by counting the number of decision points in a method, plus one for the method entry. Decision points are places where the control flow jumps to another place in the @@ -783,7 +813,7 @@ methods' complexities reaches 40, even if none of the methods was directly repor

Reported methods should be broken down into several smaller methods. Reported classes should probably be broken down into subcomponents.

Example

-

 public class Complicated {
+

 public class Complicated {
    public void example() { // This method has a cyclomatic complexity of 12
      int x = 0, y = 1, z = 2, t = 2;
      boolean a = false, b = true, c = false, d = true;
@@ -806,7 +836,7 @@ into subcomponents.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -815,10 +845,11 @@ into subcomponents.

Debugs should use logging level category/apex/bestpractices.xml/DebugsShouldUseLoggingLevel MAJOR - The first parameter of System.debug, when using the signature with two parameters, is a LoggingLevel enum.

+ Title of issues: Calls to System.debug should specify a logging level. +

The first parameter of System.debug, when using the signature with two parameters, is a LoggingLevel enum.

Having the Logging Level specified provides a cleaner log, and improves readability of it.

Example

-

 @isTest
+

 @isTest
  public class Foo {
      @isTest
      static void bar() {
@@ -827,7 +858,7 @@ into subcomponents.

System.debug(LoggingLevel.DEBUG, 'Hey, something happened.'); // not good when on strict mode } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -839,10 +870,11 @@ into subcomponents.

EagerlyLoadedDescribeSObjectResult - Eagerly loaded describe s object result + Eagerly loaded describe SObject result category/apex/performance.xml/EagerlyLoadedDescribeSObjectResult MAJOR - This rule finds DescribeSObjectResults which could have been loaded eagerly via SObjectType.getDescribe().

+ Title of issues: DescribeSObjectResult could be being loaded eagerly with all child relationships. +

This rule finds DescribeSObjectResults which could have been loaded eagerly via SObjectType.getDescribe().

When using SObjectType.getDescribe() or Schema.describeSObjects() without supplying a SObjectDescribeOptions, implicitly it will be using SObjectDescribeOptions.DEFAULT and then all child relationships will be loaded eagerly regardless whether this information is needed or not. @@ -857,14 +889,14 @@ to be consistent across different contexts and API versions.

Properties:

  • noDefault: The behavior of SObjectDescribeOptions.DEFAULT changes from API Version 43 to 44: With API Version 43, the attributes are loaded eagerly. With API Version 44, they are loaded lazily. Simply using SObjectDescribeOptions.DEFAULT doesn't automatically make use of lazy loading. (unless "Use Improved Schema Caching" critical update is applied, SObjectDescribeOptions.DEFAULT does fallback to lazy loading) With this property enabled, such usages are found. You might ignore this, if you can make sure, that you don't run a mix of API Versions.

Example

-

 public class Foo {
+

 public class Foo {
      public static void bar(List<Account> accounts) {
          if (Account.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).isCreateable()) {
              insert accounts;
          }
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd performance @@ -879,11 +911,12 @@ to be consistent across different contexts and API versions.

Empty catch block category/apex/errorprone.xml/EmptyCatchBlock MAJOR - Empty Catch Block finds instances where an exception is caught, but nothing is done. + Title of issues: Avoid empty catch blocks +

Empty Catch Block finds instances where an exception is caught, but nothing is done. In most circumstances, this swallows an exception which should either be acted on or reported.

Example

-

 public void doSomething() {
+

 public void doSomething() {
      ...
      try {
          insert accounts;
@@ -891,7 +924,7 @@ to be consistent across different contexts and API versions.

// not good } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -912,16 +945,17 @@ to be consistent across different contexts and API versions.

Empty if stmt category/apex/errorprone.xml/EmptyIfStmt MAJOR - Empty If Statement finds instances where a condition is checked but nothing is done about it.

+ Title of issues: Avoid empty 'if' statements +

Empty If Statement finds instances where a condition is checked but nothing is done about it.

Example

-

 public class Foo {
+

 public class Foo {
      public void bar(Integer x) {
          if (x == 0) {
              // empty!
          }
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -930,9 +964,10 @@ to be consistent across different contexts and API versions.

Empty statement block category/apex/errorprone.xml/EmptyStatementBlock MAJOR - Empty block statements serve no purpose and should be removed.

+ Title of issues: Avoid empty block statements. +

Empty block statements serve no purpose and should be removed.

Example

-

 public class Foo {
+

 public class Foo {
  
     private Integer _bar;
  
@@ -941,7 +976,7 @@ to be consistent across different contexts and API versions.

} }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -962,9 +997,10 @@ to be consistent across different contexts and API versions.

Empty try or finally block category/apex/errorprone.xml/EmptyTryOrFinallyBlock MAJOR - Avoid empty try or finally blocks - what's the point?

+ Title of issues: Avoid empty try or finally blocks +

Avoid empty try or finally blocks - what's the point?

Example

-

 public class Foo {
+

 public class Foo {
      public void bar() {
          try {
            // empty !
@@ -983,7 +1019,7 @@ to be consistent across different contexts and API versions.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -992,16 +1028,17 @@ to be consistent across different contexts and API versions.

Empty while stmt category/apex/errorprone.xml/EmptyWhileStmt MAJOR - Empty While Statement finds all instances where a while statement does nothing. + Title of issues: Avoid empty 'while' statements +

Empty While Statement finds all instances where a while statement does nothing. If it is a timing loop, then you should use Thread.sleep() for it; if it is a while loop that does a lot in the exit expression, rewrite it to make it clearer.

Example

-

 public void bar(Integer a, Integer b) {
+

 public void bar(Integer a, Integer b) {
    while (a == b) {
      // empty!
    }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -1010,11 +1047,12 @@ to be consistent across different contexts and API versions.

Excessive class length category/apex/design.xml/ExcessiveClassLength MAJOR - Excessive class file lengths are usually indications that the class may be burdened with excessive + Title of issues: Avoid really long classes. +

Excessive class file lengths are usually indications that the class may be burdened with excessive responsibilities that could be provided by external classes or functions. In breaking these methods apart the code becomes more managable and ripe for reuse.

Example

-

 public class Foo {
+

 public class Foo {
      public void bar1() {
          // 1000 lines of code
      }
@@ -1028,7 +1066,7 @@ apart the code becomes more managable and ripe for reuse.

// 1000 lines of code } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1037,10 +1075,11 @@ apart the code becomes more managable and ripe for reuse.

Excessive parameter list category/apex/design.xml/ExcessiveParameterList MAJOR - Methods with numerous parameters are a challenge to maintain, especially if most of them share the + Title of issues: Avoid long parameter lists. +

Methods with numerous parameters are a challenge to maintain, especially if most of them share the same datatype. These situations usually denote the need for new objects to wrap the numerous parameters.

Example

-

 // too many arguments liable to be mixed up
+

 // too many arguments liable to be mixed up
  public void addPerson(Integer birthYear, Integer birthMonth, Integer birthDate, Integer height, Integer weight, Integer ssn) {
      // ...
  }
@@ -1048,7 +1087,7 @@ same datatype. These situations usually denote the need for new objects to wrap
  public void addPerson(Date birthdate, BodyMeasurements measurements, int ssn) {
      // ...
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1057,12 +1096,13 @@ same datatype. These situations usually denote the need for new objects to wrap Excessive public count category/apex/design.xml/ExcessivePublicCount MAJOR - Classes with large numbers of public methods, attributes, and properties require disproportionate testing efforts + Title of issues: The class {0} has {1} public methods, attributes, and properties (limit: {2}) +

Classes with large numbers of public methods, attributes, and properties require disproportionate testing efforts since combinatorial side effects grow rapidly and increase risk. Refactoring these classes into smaller ones not only increases testability and reliability but also allows new variations to be developed easily.

Example

-

 public class Foo {
+

 public class Foo {
      public String value;
      public Bar something;
      public Variable var;
@@ -1076,7 +1116,7 @@ developed easily.

public String property1 { get; set; } // [... more more public properties ...] }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1085,9 +1125,10 @@ developed easily.

Field declarations should be at start category/apex/codestyle.xml/FieldDeclarationsShouldBeAtStart MAJOR - Field declarations should appear before method declarations within a class.

+ Title of issues: Field declaration for '{0}' should be before method declarations in its class +

Field declarations should appear before method declarations within a class.

Example

-

 class Foo {
+

 class Foo {
      public Integer someField; // good
  
      public void someMethod() {
@@ -1095,7 +1136,7 @@ developed easily.

public Integer anotherField; // bad }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1104,17 +1145,18 @@ developed easily.

Field naming conventions category/apex/codestyle.xml/FieldNamingConventions BLOCKER - Configurable naming conventions for field declarations. This rule reports variable declarations + Title of issues: The {0} name '{1}' doesn't match '{2}' +

Configurable naming conventions for field declarations. This rule reports variable declarations which do not match the regex that applies to their specific kind ---e.g. constants (static final), static field, final field. Each regex can be configured through properties.

By default this rule uses the standard Apex naming convention (Camel case).

Example

-

 public class Foo {
+

 public class Foo {
      Integer instanceField; // This is in camel case, so it's ok
  
      Integer INSTANCE_FIELD; // This will be reported unless you change the regex
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1123,17 +1165,18 @@ developed easily.

For loops must use braces category/apex/codestyle.xml/ForLoopsMustUseBraces MAJOR - Avoid using 'for' statements without using surrounding braces. If the code formatting or + Title of issues: Avoid using 'for' statements without curly braces +

Avoid using 'for' statements without using surrounding braces. If the code formatting or indentation is lost then it becomes difficult to separate the code being controlled from the rest.

Example

-

 for (int i = 0; i < 42; i++) // not recommended
+

 for (int i = 0; i < 42; i++) // not recommended
      foo();
  
  for (int i = 0; i < 42; i++) { // preferred approach
      foo();
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1142,18 +1185,19 @@ from the rest.

Formal parameter naming conventions category/apex/codestyle.xml/FormalParameterNamingConventions BLOCKER - Configurable naming conventions for formal parameters of methods. + Title of issues: The {0} name '{1}' doesn't match '{2}' +

Configurable naming conventions for formal parameters of methods. This rule reports formal parameters which do not match the regex that applies to their specific kind (e.g. method parameter, or final method parameter). Each regex can be configured through properties.

By default this rule uses the standard Apex naming convention (Camel case).

Example

-

 public class Foo {
+

 public class Foo {
      public bar(Integer methodParameter) { } // This is in camel case, so it's ok
  
      public baz(Integer METHOD_PARAMETER) { } // This will be reported unless you change the regex
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1162,11 +1206,12 @@ from the rest.

If else stmts must use braces category/apex/codestyle.xml/IfElseStmtsMustUseBraces MAJOR - Avoid using if..else statements without using surrounding braces. If the code formatting + Title of issues: Avoid using 'if...else' statements without curly braces +

Avoid using if..else statements without using surrounding braces. If the code formatting or indentation is lost then it becomes difficult to separate the code being controlled from the rest.

Example

-

 // this is OK
+

 // this is OK
  if (foo) x++;
  
  // but this is not
@@ -1174,7 +1219,7 @@ from the rest.

x = x+1; else x = x-1;

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1183,17 +1228,18 @@ from the rest.

If stmts must use braces category/apex/codestyle.xml/IfStmtsMustUseBraces MAJOR - Avoid using if statements without using braces to surround the code block. If the code + Title of issues: Avoid using if statements without curly braces +

Avoid using if statements without using braces to surround the code block. If the code formatting or indentation is lost then it becomes difficult to separate the code being controlled from the rest.

Example

-

 if (foo)    // not recommended
+

 if (foo)    // not recommended
      x++;
  
  if (foo) {  // preferred approach
      x++;
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1202,12 +1248,13 @@ controlled from the rest.

Inaccessible aura enabled getter category/apex/errorprone.xml/InaccessibleAuraEnabledGetter MAJOR - In the Summer '21 release, a mandatory security update enforces access modifiers on Apex properties in + Title of issues: AuraEnabled getter must be public or global if is referenced in Lightning components +

In the Summer '21 release, a mandatory security update enforces access modifiers on Apex properties in Lightning component markup. The update prevents access to private or protected Apex getters from Aura and Lightning Web Components.

Examples

Example 1

-

 public class Foo {
+

 public class Foo {
      @AuraEnabled
      public Integer counter { private get; set; } // Violating - Private getter is inaccessible to Lightning components
  
@@ -1220,7 +1267,7 @@ controlled from the rest.

} }

Example 2

-

 public class Foo {
+

 public class Foo {
      @AuraEnabled
      public Integer counter { protected get; set; } // Violating - Protected getter is inaccessible to Lightning components
  
@@ -1233,7 +1280,7 @@ controlled from the rest.

} }

Example 3

-

 public class Foo {
+

 public class Foo {
      @AuraEnabled
      public Integer counter { get; set; } // Compliant - Public getter is accessible to Lightning components
  
@@ -1245,7 +1292,7 @@ controlled from the rest.

return foo; } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -1254,20 +1301,21 @@ controlled from the rest.

Local variable naming conventions category/apex/codestyle.xml/LocalVariableNamingConventions BLOCKER - Configurable naming conventions for local variable declarations. + Title of issues: The {0} name '{1}' doesn't match '{2}' +

Configurable naming conventions for local variable declarations. This rule reports variable declarations which do not match the regex that applies to their specific kind (e.g. local variable, or final local variable). Each regex can be configured through properties.

By default this rule uses the standard Apex naming convention (Camel case).

Example

-

 public class Foo {
+

 public class Foo {
      public Foo() {
          Integer localVariable; // This is in camel case, so it's ok
  
          Integer LOCAL_VARIABLE; // This will be reported unless you change the regex
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1276,17 +1324,18 @@ controlled from the rest.

Method naming conventions category/apex/codestyle.xml/MethodNamingConventions BLOCKER - Configurable naming conventions for method declarations. This rule reports + Title of issues: The {0} name '{1}' doesn't match '{2}' +

Configurable naming conventions for method declarations. This rule reports method declarations which do not match the regex that applies to their specific kind (e.g. static method, or test method). Each regex can be configured through properties.

By default this rule uses the standard Apex naming convention (Camel case).

Example

-

 public class Foo {
+

 public class Foo {
      public void instanceMethod() { } // This is in camel case, so it's ok
  
      public void INSTANCE_METHOD() { } // This will be reported unless you change the regex

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1295,15 +1344,16 @@ controlled from the rest.

Method with same name as enclosing class category/apex/errorprone.xml/MethodWithSameNameAsEnclosingClass MAJOR - Non-constructor methods should not have the same name as the enclosing class.

+ Title of issues: Classes should not have non-constructor methods with the same name as the class +

Non-constructor methods should not have the same name as the enclosing class.

Example

-

 public class MyClass {
+

 public class MyClass {
      // this is OK because it is a constructor
      public MyClass() {}
      // this is bad because it is a method
      public void MyClass() {}
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -1312,11 +1362,12 @@ controlled from the rest.

Ncss constructor count category/apex/design.xml/NcssConstructorCount MAJOR - This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines + Title of issues: The constructor has an NCSS line count of {0} +

This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines of code for a given constructor. NCSS ignores comments, and counts actual statements. Using this algorithm, lines of code that are split are counted as one.

Example

-

 public class Foo extends Bar {
+

 public class Foo extends Bar {
      //this constructor only has 1 NCSS lines
      public Foo() {
          super();
@@ -1327,7 +1378,7 @@ lines of code that are split are counted as one.

super.foo(); } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1336,11 +1387,12 @@ lines of code that are split are counted as one.

Ncss method count category/apex/design.xml/NcssMethodCount MAJOR - This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines + Title of issues: The method '{0}()' has an NCSS line count of {1} (limit: {2}) +

This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines of code for a given method. NCSS ignores comments, and counts actual statements. Using this algorithm, lines of code that are split are counted as one.

Example

-

 public class Foo extends Bar {
+

 public class Foo extends Bar {
      //this method only has 1 NCSS lines
      public Integer method() {
          super.method();
@@ -1350,7 +1402,7 @@ lines of code that are split are counted as one.

return 1; } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1359,11 +1411,12 @@ lines of code that are split are counted as one.

Ncss type count category/apex/design.xml/NcssTypeCount MAJOR - This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines + Title of issues: The type has an NCSS line count of {0} +

This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines of code for a given type. NCSS ignores comments, and counts actual statements. Using this algorithm, lines of code that are split are counted as one.

Example

-

 //this class only has 6 NCSS lines
+

 //this class only has 6 NCSS lines
  public class Foo extends Bar {
      public Foo() {
          super();
@@ -1375,7 +1428,7 @@ lines of code that are split are counted as one.

super.foo(); } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1384,17 +1437,18 @@ lines of code that are split are counted as one.

One declaration per line category/apex/codestyle.xml/OneDeclarationPerLine BLOCKER - Apex allows the use of several variables declaration of the same type on one line. However, it + Title of issues: Use one statement for each line, it enhances code readability. +

Apex allows the use of several variables declaration of the same type on one line. However, it can lead to quite messy code. This rule looks for several declarations on the same line.

Example

-

 Integer a, b;   // not recommended
+

 Integer a, b;   // not recommended
  
  Integer a,
          b;      // ok by default, can be flagged setting the strictMode property
  
  Integer a;      // preferred approach
  Integer b;

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1415,7 +1469,8 @@ can lead to quite messy code. This rule looks for several declarations on the sa Operation with high cost in loop category/apex/performance.xml/OperationWithHighCostInLoop MAJOR - This rule finds method calls inside loops that are known to be likely a performance issue. These methods should be + Title of issues: Avoid operations in loops that may impact performances +

This rule finds method calls inside loops that are known to be likely a performance issue. These methods should be called only once before the loop.

Schema class methods like Schema.getGlobalDescribe() and Schema.describeSObjects() @@ -1423,7 +1478,7 @@ might be slow depending on the size of your organization. Calling these methods a potential performance issue.

Examples

Example 1

-

 public class GlobalDescribeExample {
+

 public class GlobalDescribeExample {
      // incorrect example
      public void getGlobalDescribeInLoop() {
          Set<String> fieldNameSet = new Set<String> {'Id'};
@@ -1447,7 +1502,7 @@ a potential performance issue.

} }

Example 2

-

 public class DescribeSObjectsExample {
+

 public class DescribeSObjectsExample {
      // incorrect example
      public void describeSObjectsInLoop() {
          Set<String> fieldNameSet = new Set<String> {'Id'};
@@ -1470,7 +1525,7 @@ a potential performance issue.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd performance @@ -1479,9 +1534,10 @@ a potential performance issue.

Operation with limits in loop category/apex/performance.xml/OperationWithLimitsInLoop MAJOR - Database class methods, DML operations, SOQL queries, SOSL queries, Approval class methods, Email sending, async scheduling or queueing within loops can cause governor limit exceptions. Instead, try to batch up the data into a list and invoke the operation once on that list of data outside the loop.

+ Title of issues: Avoid operations in loops that may hit governor limits +

Database class methods, DML operations, SOQL queries, SOSL queries, Approval class methods, Email sending, async scheduling or queueing within loops can cause governor limit exceptions. Instead, try to batch up the data into a list and invoke the operation once on that list of data outside the loop.

Example

-

 public class Something {
+

 public class Something {
      public void databaseMethodInsideOfLoop(List<Account> accounts) {
          for (Account a : accounts) {
              Database.insert(a);
@@ -1534,7 +1590,7 @@ a potential performance issue.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd performance @@ -1543,12 +1599,13 @@ a potential performance issue.

Override both equals and hashcode category/apex/errorprone.xml/OverrideBothEqualsAndHashcode MAJOR - Override both public Boolean equals(Object obj), and public Integer hashCode(), or override neither. + Title of issues: Ensure you override both equals() and hashCode() +

Override both public Boolean equals(Object obj), and public Integer hashCode(), or override neither. Even if you are inheriting a hashCode() from a parent class, consider implementing hashCode and explicitly delegating to your superclass.

This is especially important when Using Custom Types in Map Keys and Sets.

Example

-

 public class Bar {        // poor, missing a hashCode() method
+

 public class Bar {        // poor, missing a hashCode() method
      public Boolean equals(Object o) {
        // do some comparison
      }
@@ -1566,7 +1623,7 @@ a potential performance issue.

// return some hash value } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -1575,18 +1632,19 @@ a potential performance issue.

Property naming conventions category/apex/codestyle.xml/PropertyNamingConventions BLOCKER - Configurable naming conventions for property declarations. This rule reports + Title of issues: The {0} name '{1}' doesn't match '{2}' +

Configurable naming conventions for property declarations. This rule reports property declarations which do not match the regex that applies to their specific kind (e.g. static property, or instance property). Each regex can be configured through properties.

By default this rule uses the standard Apex naming convention (Camel case).

Example

-

 public class Foo {
+

 public class Foo {
      public Integer instanceProperty { get; set; } // This is in camel case, so it's ok
  
      public Integer INSTANCE_PROPERTY { get; set; } // This will be reported unless you change the regex
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle @@ -1595,11 +1653,12 @@ a potential performance issue.

Queueable without finalizer category/apex/bestpractices.xml/QueueableWithoutFinalizer INFO - Detects when the Queueable interface is used but a Finalizer is not attached. + Title of issues: This Queueable doesn't attach a Finalizer +

Detects when the Queueable interface is used but a Finalizer is not attached. It is best practice to call the System.attachFinalizer(Finalizer f) method within the execute method of a class which implements the Queueable interface. Without attaching a Finalizer, there is no way of designing error recovery actions should the Queueable action fail.

Example

-

 // Incorrect code, does not attach a finalizer.
+

 // Incorrect code, does not attach a finalizer.
  public class UserUpdater implements Queueable {
      public List<User> usersToUpdate;
  
@@ -1633,7 +1692,7 @@ Without attaching a Finalizer, there is no way of designing error recovery actio
          }
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -1642,12 +1701,13 @@ Without attaching a Finalizer, there is no way of designing error recovery actio Std cyclomatic complexity category/apex/design.xml/StdCyclomaticComplexity MAJOR - Complexity directly affects maintenance costs is determined by the number of decision points in a method + Title of issues: The {0} '{1}' has a Standard Cyclomatic Complexity of {2}. +

Complexity directly affects maintenance costs is determined by the number of decision points in a method plus one for the method entry. The decision points include 'if', 'while', 'for', and 'case labels' calls. Generally, numbers ranging from 1-4 denote low complexity, 5-7 denote moderate complexity, 8-10 denote high complexity, and 11+ is very high complexity.

Example

-

 // This has a Cyclomatic Complexity = 12
+

 // This has a Cyclomatic Complexity = 12
  public class Foo {
  1   public void example() {
  2   if (a == b || (c == d && e == f)) {
@@ -1683,7 +1743,7 @@ high complexity, and 11+ is very high complexity.

} } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1692,14 +1752,15 @@ high complexity, and 11+ is very high complexity.

Test methods must be in test classes category/apex/errorprone.xml/TestMethodsMustBeInTestClasses MAJOR - Test methods marked as a testMethod or annotated with @IsTest, + Title of issues: Test methods must be in test classes +

Test methods marked as a testMethod or annotated with @IsTest, but not residing in a test class should be moved to a proper class or have the @IsTest annotation added to the class.

Support for tests inside functional classes was removed in Spring-13 (API Version 27.0), making classes that violate this rule fail compile-time. This rule is mostly usable when dealing with legacy code.

Example

-

 // Violating
+

 // Violating
  private class TestClass {
    @IsTest static void myTest() {
      // Code here
@@ -1726,7 +1787,7 @@ high complexity, and 11+ is very high complexity.

// Code here } }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -1735,11 +1796,12 @@ high complexity, and 11+ is very high complexity.

Too many fields category/apex/design.xml/TooManyFields MAJOR - Classes that have too many fields can become unwieldy and could be redesigned to have fewer fields, + Title of issues: Too many fields +

Classes that have too many fields can become unwieldy and could be redesigned to have fewer fields, possibly through grouping related fields in new objects. For example, a class with individual city/state/zip fields could park them within a single Address field.

Example

-

 public class Person {
+

 public class Person {
      // too many separate fields
      Integer birthYear;
      Integer birthMonth;
@@ -1753,7 +1815,7 @@ city/state/zip fields could park them within a single Address field.

Date birthDate; BodyMeasurements measurements; }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1762,7 +1824,8 @@ city/state/zip fields could park them within a single Address field.

Type shadows built in namespace category/apex/errorprone.xml/TypeShadowsBuiltInNamespace BLOCKER - This rule finds Apex classes, enums, and interfaces that have the same name as a class, enum, or interface in the System + Title of issues: This name causes a collision with a class, enum, or interface used in the <code>{0}</code> namespace. Please choose a different name. +

This rule finds Apex classes, enums, and interfaces that have the same name as a class, enum, or interface in the System or Schema namespace. Shadowing these namespaces in this way can lead to confusion and unexpected behavior. Code that intends to reference a System or Schema class, enum, or interface may inadvertently reference the locally defined type instead. @@ -1775,13 +1838,13 @@ city/state/zip fields could park them within a single Address field.

As Salesforce introduces new types into the System and Schema namespaces, the rule might not always recognize the new types and produce false-negatives und the standard types are updated.

Example

-

 // Violation: Causes a collision with the `System.Database` class.
+

 // Violation: Causes a collision with the `System.Database` class.
  public class Database {
      public static String query() {
          return 'Hello World';
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd errorprone @@ -1790,15 +1853,16 @@ city/state/zip fields could park them within a single Address field.

Unused local variable category/apex/bestpractices.xml/UnusedLocalVariable MAJOR - Detects when a local variable is declared and/or assigned but not used.

+ Title of issues: Variable '{0}' defined but not used +

Detects when a local variable is declared and/or assigned but not used.

Example

-

 public Boolean bar(String z) {
+

 public Boolean bar(String z) {
          String x = 'some string'; // not used
  
          String y = 'some other string'; // used in the next line
          return z.equals(y);
      }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd bestpractices @@ -1807,7 +1871,8 @@ city/state/zip fields could park them within a single Address field.

Unused method category/apex/design.xml/UnusedMethod MAJOR - Avoid having unused methods since they make understanding and maintaining code harder.

+ Title of issues: Unused methods make understanding code harder +

Avoid having unused methods since they make understanding and maintaining code harder.

This rule finds not only unused private methods, but public methods as well, as long as the class itself is not entirely unused. A class is considered used, if it contains at least one other method/variable declaration that is used, as shown in the @@ -1842,7 +1907,7 @@ well-formed sfdx-project.json: } }

Example

-

 public class Triangle {
+

 public class Triangle {
      private Double side1;
      private Double side2;
      private Double side3;
@@ -1858,7 +1923,7 @@ well-formed sfdx-project.json:
          return (side1 + side2 + side3)/2;
      }
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd design @@ -1867,17 +1932,18 @@ well-formed sfdx-project.json: While loops must use braces category/apex/codestyle.xml/WhileLoopsMustUseBraces MAJOR - Avoid using 'while' statements without using braces to surround the code block. If the code + Title of issues: Avoid using 'while' statements without curly braces +

Avoid using 'while' statements without using braces to surround the code block. If the code formatting or indentation is lost then it becomes difficult to separate the code being controlled from the rest.

Example

-

 while (true)    // not recommended
+

 while (true)    // not recommended
      x++;
  
  while (true) {  // preferred approach
      x++;
  }

-

Full documentation: PMD rule documentation

]]> +

Full documentation

]]> pmd codestyle diff --git a/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-java.xml b/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-java.xml index b652776a..1ad255b9 100644 --- a/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-java.xml +++ b/sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd/rules-java.xml @@ -7558,7 +7558,7 @@ We'd like to report a violation each time a declaration of Factory is not declar

 import io.factories.Factory;
 
-public class a {
+public class Foo {
   Factory f1;
 
   void myMethod() {