diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 48136fc9..fd78fab9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -105,6 +105,12 @@ jobs:
name: sonar-pmd-plugin-${{ env.TAG_NAME }}${{ env.ARTIFACT_SUFFIX }}
path: sonar-pmd-plugin/target/sonar-pmd-plugin-*.jar
+ - name: Upload sonar-pmd-apex-plugin jar
+ uses: actions/upload-artifact@v4
+ with:
+ name: sonar-pmd-apex-plugin-${{ env.TAG_NAME }}${{ env.ARTIFACT_SUFFIX }}
+ path: sonar-pmd-apex-plugin/target/sonar-pmd-apex-plugin-*.jar
+
- name: Upload sonar-pmd-lib jar
uses: actions/upload-artifact@v4
with:
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
new file mode 100644
index 00000000..1fb9ef97
--- /dev/null
+++ b/.junie/guidelines.md
@@ -0,0 +1,71 @@
+# 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.17.0
+- Java source compatibility: 8 to 25 (including 25-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.
+
+## Generate new rules files
+Use maven command:
+
+ ./mvnw generate-resources -Pgenerate-pmd-rules -pl sonar-pmd-plugin
+
+## Integration tests
+The integration tests are run using the SonarQube Docker image.
+Do not run the integration tests during Junie sessions unless it is the very last step to verify the plugins.
diff --git a/README.md b/README.md
index bb6a3618..4e8ba04e 100644
--- a/README.md
+++ b/README.md
@@ -99,12 +99,19 @@ Sonar-PMD is licensed under the [GNU Lesser General Public License, Version 3.0]
Parts of the rule descriptions displayed in SonarQube have been extracted from [PMD](https://pmd.github.io/) and are licensed under a [BSD-style license](https://github.com/pmd/pmd/blob/master/LICENSE).
## Build and test the plugin
-To build the plugin and run the integration tests (use java 17 to build the plugin):
+To build the plugin and run the integration tests (use Java 17 to build the plugin):
./mvnw clean verify
-## Generate PMD rules XML (Java and Kotlin)
-To regenerate the `rules-java.xml` and `rules-kotlin.xml` from PMD 7 using the provided Groovy script, run from the project root:
+### Failing Integration Tests
+
+If you experience issues starting the integration tests, make sure there is enough free space
+on your disk. Otherwise, ElasticSearch used by SonarQube might not be able to start due to bad
+system health status (high disk watermark [90%] exceeded).
+
+## Generate PMD rules XML (Java, Kotlin and Apex)
+To regenerate the `rules-java.xml`, `rules-kotlin.xml` and `rules-apex.xml` from PMD 7
+using the provided Groovy script, run from the project root:
./mvnw clean generate-resources -Pgenerate-pmd-rules -pl sonar-pmd-plugin -am
diff --git a/integration-test/pom.xml b/integration-test/pom.xml
index cdad522a..52fb83ca 100644
--- a/integration-test/pom.xml
+++ b/integration-test/pom.xml
@@ -41,10 +41,12 @@
org.sonarsource.orchestratorsonar-orchestrator
+ testorg.sonarsource.orchestratorsonar-orchestrator-junit4
+ testorg.codehaus.sonar
@@ -66,6 +68,12 @@
sonar-pmd-lib${revision}
+
+ org.sonarsource.java
+ java-frontend
+ ${sonar-java.version}
+ provided
+
diff --git a/integration-test/projects/pmd-apex-rules/pom.xml b/integration-test/projects/pmd-apex-rules/pom.xml
new file mode 100644
index 00000000..d4ff3df1
--- /dev/null
+++ b/integration-test/projects/pmd-apex-rules/pom.xml
@@ -0,0 +1,38 @@
+
+ 4.0.0
+
+ com.sonarsource.it.projects
+ pmd-apex-rules
+ 1.0-SNAPSHOT
+
+ Integration test Apex project
+ Simple Apex project for PMD Apex sanity test
+
+
+
+
+
+ org.sonarsource.scanner.maven
+ sonar-maven-plugin
+ 5.2.0.4988
+
+
+
+
+
+
+
+ skipSonar
+
+
+ skipTestProjects
+ true
+
+
+
+ true
+
+
+
+
diff --git a/integration-test/projects/pmd-apex-rules/src/main/apex/BooleanViolation.cls b/integration-test/projects/pmd-apex-rules/src/main/apex/BooleanViolation.cls
new file mode 100644
index 00000000..4000db9c
--- /dev/null
+++ b/integration-test/projects/pmd-apex-rules/src/main/apex/BooleanViolation.cls
@@ -0,0 +1,10 @@
+// Violates the rule: Uses a Boolean parameter
+public class BooleanViolation {
+ public static void doSomething(Boolean isSomething) {
+ if (isSomething == true) {
+ isSomething = false;
+ } else {
+ isSomething = null;
+ }
+ }
+}
diff --git a/integration-test/projects/pmd-apex-rules/src/main/apex/WeakCrypto.cls b/integration-test/projects/pmd-apex-rules/src/main/apex/WeakCrypto.cls
new file mode 100644
index 00000000..e45d0249
--- /dev/null
+++ b/integration-test/projects/pmd-apex-rules/src/main/apex/WeakCrypto.cls
@@ -0,0 +1,8 @@
+public class WeakCrypto {
+ public WeakCrypto() {
+ Blob exampleIv = Blob.valueOf('0000000000000000');
+ Blob key = Crypto.generateAesKey(128);
+ Blob data = Blob.valueOf('Data to be encrypted');
+ Blob encrypted = Crypto.encrypt('AES128', key, exampleIv, data);
+ }
+}
diff --git a/integration-test/projects/pmd-avoid-duplicate-literals/pom.xml b/integration-test/projects/pmd-avoid-duplicate-literals/pom.xml
index b743b0d0..d1e72d21 100644
--- a/integration-test/projects/pmd-avoid-duplicate-literals/pom.xml
+++ b/integration-test/projects/pmd-avoid-duplicate-literals/pom.xml
@@ -12,7 +12,7 @@
org.sonarsource.scanner.mavensonar-maven-plugin
- 5.1.0.4751
+ 5.2.0.4988
diff --git a/integration-test/projects/pmd-extensions/pom.xml b/integration-test/projects/pmd-extensions/pom.xml
index ee55aab3..e68f256c 100644
--- a/integration-test/projects/pmd-extensions/pom.xml
+++ b/integration-test/projects/pmd-extensions/pom.xml
@@ -17,7 +17,7 @@
org.sonarsource.scanner.mavensonar-maven-plugin
- 5.1.0.4751
+ 5.2.0.4988
diff --git a/integration-test/src/main/java/org/sonar/examples/pmd/MaximumMethodsCountCheck.java b/integration-test/src/main/java/org/sonar/examples/pmd/MaximumMethodsCountCheck.java
index 753ffb31..0d18ba3b 100644
--- a/integration-test/src/main/java/org/sonar/examples/pmd/MaximumMethodsCountCheck.java
+++ b/integration-test/src/main/java/org/sonar/examples/pmd/MaximumMethodsCountCheck.java
@@ -24,6 +24,7 @@
import net.sourceforge.pmd.lang.java.ast.ASTClassBody;
+import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.properties.NumericConstraints;
@@ -48,22 +49,24 @@ public MaximumMethodsCountCheck() {
definePropertyDescriptor(propertyDescriptor);
}
- @Override
- public void start(RuleContext ctx) {
- LOG.info("Start " + getName());
- }
-
- @Override
- public void end(RuleContext ctx) {
- LOG.info("End " + getName());
- }
+// @Override
+// public void start(RuleContext ctx) {
+// LOG.info("Start {}", getName());
+// }
+//
+// @Override
+// public void end(RuleContext ctx) {
+// LOG.info("End {}", getName());
+// }
@Override
- public Object visit(ASTClassBody node, Object data) {
- List methods = node.descendants(ASTMethodDeclaration.class).toList();
+ public Object visit(ASTCompilationUnit cUnit, Object data) {
+ LOG.info("Start {}", getName());
+ List methods = cUnit.descendants(ASTMethodDeclaration.class).toList();
if (methods.size() > getProperty(propertyDescriptor)) {
- asCtx(data).addViolation(node);
+ asCtx(data).addViolation(cUnit);
}
- return super.visit(node, data);
+ LOG.info("End {}", getName());
+ return super.visit(cUnit, data);
}
}
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/main/resources/org/sonar/examples/pmd/rulesets.xml b/integration-test/src/main/resources/org/sonar/examples/pmd/rulesets.xml
index 6aa11ea9..16c970e5 100644
--- a/integration-test/src/main/resources/org/sonar/examples/pmd/rulesets.xml
+++ b/integration-test/src/main/resources/org/sonar/examples/pmd/rulesets.xml
@@ -89,19 +89,20 @@
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"
language="java">
- IOException should never be extended. Either use it, or extend Exception for your own business exceptions.
+ IOException should never be extended. Either use it or extend Exception for your own business exceptions.
issues1 = ORCHESTRATOR.retrieveIssues(IssueQuery.create()
+ .components(keyFor(projectName, "src/main/apex", "", "BooleanViolation", ".cls")));
+
+ assertThat(issues1)
+ .as("Expect at least one Apex issue to be reported for BooleanViolation.cls")
+ .isNotEmpty();
+
+ // Preferably, we find BooleanViolation rule
+ assertThat(issues1.stream().anyMatch(i -> "pmd-apex:AvoidBooleanMethodParameters".equals(i.ruleKey())))
+ .as("Expect BooleanViolation to be reported on BooleanViolation.cls")
+ .isTrue();
+
+ List issuesCrypto = ORCHESTRATOR.retrieveIssues(IssueQuery.create()
+ .components(keyFor(projectName, "src/main/apex", "", "WeakCrypto", ".cls")));
+
+ assertThat(issuesCrypto)
+ .as("Expect at least one Apex issue to be reported for WeakCrypto.cls")
+ .isNotEmpty();
+
+ // Preferably, we find ApexBadCrypto rule
+ assertThat(issuesCrypto.stream().anyMatch(i -> "pmd-apex:ApexBadCrypto".equals(i.ruleKey())))
+ .as("Expect ApexBadCrypto to be reported on WeakCrypto.cls")
+ .isTrue();
+
+ // cleanup
+ ORCHESTRATOR.resetData(projectName);
+ }
+}
diff --git a/integration-test/src/test/java/com/sonar/it/java/suite/TestUtils.java b/integration-test/src/test/java/com/sonar/it/java/suite/TestUtils.java
index a003069e..e9dacaf1 100644
--- a/integration-test/src/test/java/com/sonar/it/java/suite/TestUtils.java
+++ b/integration-test/src/test/java/com/sonar/it/java/suite/TestUtils.java
@@ -52,7 +52,10 @@ static String keyFor(String projectKey, String srcDir, String pkgDir, String cls
}
private static @NotNull String ensureEndsWithSlash(String srcDir) {
- if (!srcDir.isEmpty() && !srcDir.endsWith("/")) {
+ if (srcDir.isEmpty()) {
+ return srcDir;
+ }
+ if (!srcDir.endsWith("/")) {
srcDir = srcDir + "/";
}
return srcDir;
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 cb7e1664..d5fc33b5 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
@@ -131,6 +131,7 @@ public static PmdTestOrchestrator init() {
determineKotlinPluginVersion()
))
.addPlugin(byWildcardMavenFilename(new File("../sonar-pmd-plugin/target"), "sonar-pmd-plugin-*.jar"))
+ .addPlugin(byWildcardMavenFilename(new File("../sonar-pmd-apex-plugin/target"), "sonar-pmd-apex-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"))
@@ -138,6 +139,9 @@ public static PmdTestOrchestrator init() {
.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-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);
@@ -161,6 +165,6 @@ private static String determineKotlinPluginVersion() {
}
private static String determineSonarqubeVersion() {
- return System.getProperty(SONAR_VERSION_KEY, "LATEST_RELEASE[25.3]"); // use SQ 9.9.4 to test with old version
+ return System.getProperty(SONAR_VERSION_KEY, "LATEST_RELEASE[25.9]"); // use SQ 9.9.4 to test with old version
}
}
diff --git a/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-apex-profile.xml b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-apex-profile.xml
new file mode 100644
index 00000000..9cf52b38
--- /dev/null
+++ b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-apex-profile.xml
@@ -0,0 +1,23 @@
+
+
+
+ pmd-apex-profile
+ apex
+
+
+ pmd-apex
+ ApexAssertionsShouldIncludeMessage
+ MAJOR
+
+
+ pmd-apex
+ ApexBadCrypto
+ CRITICAL
+
+
+ pmd-apex
+ AvoidBooleanMethodParameters
+ MAJOR
+
+
+
diff --git a/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-extensions-profile.xml b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-extensions-profile.xml
index 4d04be7e..4d1881fb 100644
--- a/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-extensions-profile.xml
+++ b/integration-test/src/test/resources/com/sonar/it/java/PmdTest/pmd-extensions-profile.xml
@@ -21,12 +21,13 @@
pmd
- AvoidCatchingThrowable
+ DoNotExtendIOExceptionMAJOR
+
pmd
- DoNotExtendIOException
+ AvoidCatchingThrowableMAJOR
diff --git a/pom.xml b/pom.xml
index ec96eb25..368c6277 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,11 +108,13 @@
3.3.12.0.172.20.0
+ 1.0.0sonar-pmd-libsonar-pmd-plugin
+ sonar-pmd-apex-pluginintegration-test
@@ -179,6 +181,11 @@
commons-lang3${commons-lang3.version}
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+
@@ -486,6 +493,15 @@
+
+ with-integration-tests
+
+ false
+
+
+ integration-test
+
+
diff --git a/scripts/pmd7_rules_xml_generator.groovy b/scripts/pmd7_rules_xml_generator.groovy
index 282c480a..6721ce29 100644
--- a/scripts/pmd7_rules_xml_generator.groovy
+++ b/scripts/pmd7_rules_xml_generator.groovy
@@ -62,8 +62,11 @@ MarkdownToHtmlConverter.setPmdVersion(pmdVersion)
// Configuration
@Field def pmdJavaJarPath = new File("${System.getProperty("user.home")}/.m2/repository/net/sourceforge/pmd/pmd-java/${pmdVersion}/pmd-java-${pmdVersion}.jar")
@Field def pmdKotlinJarPath = new File("${System.getProperty("user.home")}/.m2/repository/net/sourceforge/pmd/pmd-kotlin/${pmdVersion}/pmd-kotlin-${pmdVersion}.jar")
+@Field def pmdApexJarPath = new File("${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"
+
// Define language-specific rule alternatives paths
@Field String javaRuleAlternativesPath = "scripts/rule-alternatives-java.json"
@Field String kotlinRuleAlternativesPath = "scripts/rule-alternatives-kotlin.json"
@@ -112,20 +115,34 @@ if (binding.hasVariable('TEST_MODE') && binding.getVariable('TEST_MODE')) {
return // Skip the rest of the script
}
-// Get output directory from binding variable (set by Maven) or use a default directory
-// The 'outputDir' variable is passed from Maven's groovy-maven-plugin configuration
-@Field def defaultOutputDir = new File("sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd").exists() ?
+// Get output directory from binding variable (set by Maven) or use default directories per plugin
+// The 'outputDir' variable controls Java/Kotlin output (main plugin). Optionally, 'apexOutputDir' controls Apex output (Apex plugin)
+@Field def defaultOutputDir = new File("sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd").exists() ?
"sonar-pmd-plugin/src/main/resources/org/sonar/plugins/pmd" : "."
@Field def outputDirPath = binding.hasVariable('outputDir') ? outputDir : defaultOutputDir
+@Field def apexRel = "sonar-pmd-apex-plugin/src/main/resources/org/sonar/plugins/pmd"
+@Field def defaultApexOutputDir = apexRel
+@Field def apexOutputDirPath = {
+ def override = binding.hasVariable('apexOutputDir') ? apexOutputDir : null
+ def target = override ?: defaultApexOutputDir
+ def dirFile = new File(target)
+ if (!dirFile.exists() || !dirFile.isDirectory()) {
+ throw new RuntimeException("Required Apex output directory not found: ${dirFile.absolutePath}. Set 'apexOutputDir' to override or ensure the apex plugin module exists.")
+ }
+ return target
+}()
@Field def javaOutputFileName = "rules-java.xml"
@Field def kotlinOutputFileName = "rules-kotlin.xml"
+@Field def apexOutputFileName = "rules-apex.xml"
@Field def javaOutputFilePath = new File(outputDirPath, javaOutputFileName)
@Field def kotlinOutputFilePath = new File(outputDirPath, kotlinOutputFileName)
+@Field def apexOutputFilePath = new File(apexOutputDirPath, apexOutputFileName)
logInfo("PMD ${pmdVersion} Rules XML Generator")
logInfo("=" * 50)
-logInfo("Java output file: ${javaOutputFilePath}")
+logInfo("Java output file : ${javaOutputFilePath}")
logInfo("Kotlin output file: ${kotlinOutputFilePath}")
+logInfo("Apex output file : ${apexOutputFilePath}")
// Function to read rules from a PMD JAR
def readRulesFromJar = { jarFile, categoriesPath ->
@@ -215,6 +232,11 @@ logInfo("reading Kotlin rules from ${pmdKotlinJarPath}")
def kotlinRules = readRulesFromJar(pmdKotlinJarPath, kotlinCategoriesPropertiesPath)
logInfo("found ${kotlinRules.size()} total Kotlin rules\n")
+// Read Apex rules
+logInfo("reading Apex rules from ${pmdApexJarPath}")
+def apexRules = readRulesFromJar(pmdApexJarPath, apexCategoriesPropertiesPath)
+logInfo("found ${apexRules.size()} total Apex rules\n")
+
// Helper function to escape XML content for CDATA
String escapeForCdata(String text) {
if (!text) return ""
@@ -309,7 +331,7 @@ def generateXmlFile = { outputFile, rules, language ->
}
}
}
-
+
// Print summary information
def activeRules = rules.count { !it.deprecated }
def deprecatedRules = rules.count { it.deprecated }
@@ -366,12 +388,18 @@ logInfo("Generating Kotlin rules XML file...")
logInfo("=" * 30)
def kotlinSuccess = generateXmlFile(kotlinOutputFilePath, kotlinRules, "Kotlin")
+// Generate Apex rules XML file
+logInfo("")
+logInfo("Generating Apex rules XML file...")
+logInfo("=" * 30)
+def apexSuccess = generateXmlFile(apexOutputFilePath, apexRules, "Apex")
+
// Add XPathRule as a special case to the Java rules XML file
addXPathRuleToJavaFile()
logInfo("")
-if (javaSuccess && kotlinSuccess) {
- logInfo("XML generation completed successfully for both Java and Kotlin rules!")
+if (javaSuccess && kotlinSuccess && apexSuccess) {
+ logInfo("XML generation completed successfully for both Java, Kotlin and Apex rules!")
} else {
logInfo("XML generation completed with errors. Please check the logs above.")
}
diff --git a/sonar-pmd-apex-plugin/pom.xml b/sonar-pmd-apex-plugin/pom.xml
new file mode 100644
index 00000000..a41dd166
--- /dev/null
+++ b/sonar-pmd-apex-plugin/pom.xml
@@ -0,0 +1,105 @@
+
+
+ 4.0.0
+
+
+ org.sonarsource.pmd
+ sonar-pmd
+ ${revision}
+
+
+ sonar-pmd-apex-plugin
+ sonar-plugin
+
+ SonarQube PMD Apex Plugin
+ Sonar-PMD Apex is a plugin that provides coding rules from PMD for Apex.
+ 2012
+ https://github.com/jborgers/sonar-pmd
+
+
+ true
+
+
+
+
+ org.sonarsource.api.plugin
+ sonar-plugin-api
+ provided
+
+
+ org.sonarsource.sonarqube
+ sonar-plugin-api-impl
+ test
+
+
+ org.sonarsource.pmd
+ sonar-pmd-lib
+ ${revision}
+
+
+ net.sourceforge.pmd
+ pmd-apex
+ ${pmd.version}
+
+
+ org.sonarsource.sslr-squid-bridge
+ sslr-squid-bridge
+ ${sslr.squid.bridge.version}
+
+
+ org.picocontainer
+ picocontainer
+
+
+ commons-lang
+ commons-lang
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+ com.google.guava
+ guava
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.jdom
+ jdom2
+ ${jdom2.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j-api.version}
+
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j-api.version}
+ test
+
+
+
+
+
+
+ org.sonarsource.sonar-packaging-maven-plugin
+ sonar-packaging-maven-plugin
+
+ pmd-apex
+ PMD Apex
+ org.sonar.plugins.pmd.PmdApexPlugin
+ Analyze Apex code with PMD.
+ apex
+
+
+
+
+
diff --git a/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java
new file mode 100644
index 00000000..77ae6078
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexExecutor.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ */
+package org.sonar.plugins.pmd;
+
+import net.sourceforge.pmd.reporting.Report;
+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.plugins.pmd.util.ClassLoaderUtils;
+
+import java.net.URLClassLoader;
+
+/**
+ * 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 Apex {}";
+ }
+
+ @Override
+ protected String getEndMessage() {
+ return "Execute PMD Apex {} (done) | time={}ms";
+ }
+
+ @Override
+ protected Report executePmd(URLClassLoader classLoader) {
+ return executeLanguage(classLoader, PmdConstants.LANGUAGE_APEX_KEY, PmdConstants.MAIN_APEX_REPOSITORY_KEY);
+ }
+
+ /**
+ * @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() {
+ return ClassLoaderUtils.empty();
+ }
+}
diff --git a/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexPlugin.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexPlugin.java
new file mode 100644
index 00000000..e4744ba2
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexPlugin.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ * 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.Plugin;
+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;
+
+/**
+ * Entry point for the PMD Apex plugin. Initially empty; Apex-specific components
+ * will be moved here from the main plugin in subsequent steps.
+ */
+public class PmdApexPlugin implements Plugin {
+
+ @Override
+ public void define(Context context) {
+ context.addExtensions(
+ PmdApexSensor.class,
+ PmdConfiguration.class,
+ PmdApexExecutor.class,
+ PmdApexRulesDefinition.class,
+ PmdApexSonarWayProfile.class,
+ ApexLanguage.class,
+ PmdViolationRecorder.class
+ );
+
+ context.addExtensions(ApexLanguageProperties.getProperties());
+ }
+}
diff --git a/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexSensor.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexSensor.java
new file mode 100644
index 00000000..2ead0d80
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/PmdApexSensor.java
@@ -0,0 +1,83 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ * 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.RuleViolation;
+import org.sonar.api.batch.fs.FilePredicates;
+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.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+
+/**
+ * Sensor for running PMD on Apex files only.
+ */
+public class PmdApexSensor implements Sensor {
+ private final ActiveRules profile;
+ private final PmdApexExecutor apexExecutor;
+ private final PmdViolationRecorder pmdViolationRecorder;
+ private final FileSystem fs;
+
+ public PmdApexSensor(ActiveRules profile, PmdApexExecutor apexExecutor, PmdViolationRecorder pmdViolationRecorder, FileSystem fs) {
+ this.profile = profile;
+ this.apexExecutor = apexExecutor;
+ this.pmdViolationRecorder = pmdViolationRecorder;
+ this.fs = fs;
+ }
+
+ private boolean shouldExecuteOnProject() {
+ return 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) {
+ FilePredicates predicates = fs.predicates();
+ final boolean hasMatchingFiles = fs.hasFiles(predicates.and(
+ predicates.hasLanguage(languageKey),
+ predicates.hasType(type)));
+ return hasMatchingFiles && !profile.findByRepository(repositoryKey).isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.onlyOnLanguages(PmdConstants.LANGUAGE_APEX_KEY)
+ .name("PmdApexSensor");
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ if (!shouldExecuteOnProject()) {
+ return;
+ }
+ 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-apex-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java
new file mode 100644
index 00000000..c124f9ab
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguage.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ */
+package org.sonar.plugins.pmd.languages;
+
+import org.sonar.api.config.Configuration;
+import org.sonar.api.resources.AbstractLanguage;
+import org.sonar.api.server.ServerSide;
+import org.sonar.plugins.pmd.PmdConstants;
+
+@ServerSide
+public final class ApexLanguage extends AbstractLanguage {
+
+ private final Configuration config;
+
+ public ApexLanguage(Configuration config) {
+ super(PmdConstants.LANGUAGE_APEX_KEY, PmdConstants.LANGUAGE_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;
+ }
+}
diff --git a/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java
new file mode 100644
index 00000000..d2e94987
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/languages/ApexLanguageProperties.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ */
+package org.sonar.plugins.pmd.languages;
+
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.plugins.pmd.PmdConstants;
+
+import java.util.List;
+
+public final class ApexLanguageProperties {
+
+ public static final String FILE_SUFFIXES_KEY = "sonar.apex.file.suffixes";
+ public static final String DEFAULT_FILE_SUFFIXES = ".cls,.trigger";
+
+ private ApexLanguageProperties() {
+ // static utility
+ }
+
+ 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-apex-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java
new file mode 100644
index 00000000..1cc51577
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/profile/PmdApexSonarWayProfile.java
@@ -0,0 +1,28 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ */
+package org.sonar.plugins.pmd.profile;
+
+import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
+import org.sonar.plugins.pmd.PmdConstants;
+
+public class PmdApexSonarWayProfile implements BuiltInQualityProfilesDefinition {
+
+ @Override
+ public void define(Context context) {
+ NewBuiltInQualityProfile profile = context.createBuiltInQualityProfile("Sonar way", PmdConstants.LANGUAGE_APEX_KEY);
+ profile.setDefault(true);
+
+ profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "AvoidDebugStatements");
+ profile.activateRule(PmdConstants.MAIN_APEX_REPOSITORY_KEY, "OperationWithLimitsInLoop");
+ 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-apex-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java
new file mode 100644
index 00000000..9c5f0457
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdApexRulesDefinition.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module
+ */
+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 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);
+ org.sonar.squidbridge.rules.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();
+ }
+}
diff --git a/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java
new file mode 100644
index 00000000..b33c2976
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java
@@ -0,0 +1,12 @@
+package org.sonar.plugins.pmd.xml.factory;
+
+import org.sonar.plugins.pmd.xml.PmdRuleSet;
+
+import java.io.IOException;
+
+public interface RuleSetFactory extends AutoCloseable {
+ PmdRuleSet create();
+
+ @Override
+ void close() throws IOException;
+}
diff --git a/sonar-pmd-apex-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml b/sonar-pmd-apex-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml
new file mode 100644
index 00000000..cdfb5424
--- /dev/null
+++ b/sonar-pmd-apex-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-apex-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml b/sonar-pmd-apex-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml
new file mode 100644
index 00000000..1eaea659
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/main/resources/org/sonar/plugins/pmd/rules-apex.xml
@@ -0,0 +1,2092 @@
+
+
+
+
+ AnnotationsNamingConventions
+ Annotations naming conventions
+ category/apex/codestyle.xml/AnnotationsNamingConventions
+ MINOR
+ Title of issues: The annotation @{0} should be in PascalCase: @{1}
+
Apex, while case-insensitive, benefits from a consistent code style to improve readability and maintainability.
+ Enforcing PascalCase for annotations aligns with the established conventions and reduces ambiguity - promoting a unified coding standard.
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ ApexAssertionsShouldIncludeMessage
+ Apex assertions should include message
+ category/apex/bestpractices.xml/ApexAssertionsShouldIncludeMessage
+ MAJOR
+ 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
+ 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
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+
+ ApexBadCrypto
+ Apex bad crypto
+ category/apex/security.xml/ApexBadCrypto
+ MAJOR
+ 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 {
+ 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);
+ }
]]>
+ pmd
+ security
+
+
+ ApexCRUDViolation
+ Apex CRUDViolation
+ category/apex/security.xml/ApexCRUDViolation
+ MAJOR
+ 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
+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.
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;
+ }
+ }
]]>
+ pmd
+ security
+
+
+ ApexCSRF
+ Apex CSRF
+ category/apex/errorprone.xml/ApexCSRF
+ MAJOR
+ 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.
+
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.
]]>
+ pmd
+ errorprone
+
+
+ ApexDangerousMethods
+ Apex dangerous methods
+ category/apex/security.xml/ApexDangerousMethods
+ MAJOR
+ 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 Foo() {
+ Configuration.disableTriggerCRUDSecurity();
+ }
+ }
]]>
+ pmd
+ security
+
+
+ ApexDoc
+ Apex doc
+ category/apex/documentation.xml/ApexDoc
+ MAJOR
+ 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 {
+ /**
+ * @description Bar
+ * @return Bar
+ */
+ public Object bar() { return null; }
+ }
]]>
+ pmd
+ documentation
+
+
+ ApexInsecureEndpoint
+ Apex insecure endpoint
+ category/apex/security.xml/ApexInsecureEndpoint
+ MAJOR
+ 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 {
+ void foo() {
+ HttpRequest req = new HttpRequest();
+ req.setEndpoint('http://localhost:com');
+ }
+ }
]]>
+ pmd
+ security
+
+
+ ApexOpenRedirect
+ Apex open redirect
+ category/apex/security.xml/ApexOpenRedirect
+ MAJOR
+ 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 {
+ String unsafeLocation = ApexPage.getCurrentPage().getParameters.get('url_param');
+ PageReference page() {
+ return new PageReference(unsafeLocation);
+ }
+ }
]]>
+ pmd
+ security
+
+
+ ApexSharingViolations
+ Apex sharing violations
+ category/apex/security.xml/ApexSharingViolations
+ MAJOR
+ 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 {
+ // DML operation here
+ }
]]>
+ pmd
+ security
+
+
+ ApexSuggestUsingNamedCred
+ Apex suggest using named cred
+ category/apex/security.xml/ApexSuggestUsingNamedCred
+ MAJOR
+ 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.
]]>
+ pmd
+ security
+
+
+ ApexUnitTestClassShouldHaveAsserts
+ Apex unit test class should have asserts
+ category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveAsserts
+ MAJOR
+ 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
+ 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();
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+
+ ApexUnitTestClassShouldHaveRunAs
+ Apex unit test class should have run as
+ category/apex/bestpractices.xml/ApexUnitTestClassShouldHaveRunAs
+ MAJOR
+ 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
+ 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());
+ }
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+
+ ApexUnitTestMethodShouldHaveIsTestAnnotation
+ Apex unit test method should have is test annotation
+ category/apex/bestpractices.xml/ApexUnitTestMethodShouldHaveIsTestAnnotation
+ MAJOR
+ 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.
]]>
+ pmd
+ bestpractices
+
+
+ ApexUnitTestShouldNotUseSeeAllDataTrue
+ Apex unit test should not use see all data true
+ category/apex/bestpractices.xml/ApexUnitTestShouldNotUseSeeAllDataTrue
+ MAJOR
+ 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)
+ 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();
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+
+ ApexXSSFromEscapeFalse
+ Apex XSSFrom escape false
+ category/apex/security.xml/ApexXSSFromEscapeFalse
+ MAJOR
+ 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 {
+ Trigger.new[0].addError(vulnerableHTMLGoesHere, false);
+ }
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() { ... }
]]>
+ pmd
+ design
+
+
+ AvoidDebugStatements
+ Avoid debug statements
+ category/apex/performance.xml/AvoidDebugStatements
+ MAJOR
+ 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 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
+ }
+ }
+ }
]]>
+ pmd
+ performance
+
+
+ AvoidDeeplyNestedIfStmts
+ Avoid deeply nested if stmts
+ category/apex/design.xml/AvoidDeeplyNestedIfStmts
+ MAJOR
+ 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 void bar(Integer x, Integer y, Integer z) {
+ if (x>y) {
+ if (y>z) {
+ if (z==x) {
+ // !! too deep
+ }
+ }
+ }
+ }
+ }
]]>
+ pmd
+ design
+
+
+ AvoidDirectAccessTriggerMap
+ Avoid direct access trigger Map
+ category/apex/errorprone.xml/AvoidDirectAccessTriggerMap
+ MAJOR
+ 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) {
+ 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.
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ AvoidGlobalModifier
+ Avoid global modifier
+ category/apex/bestpractices.xml/AvoidGlobalModifier
+ MAJOR
+ 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 UndeletableType unchangable(UndeletableType param) {
+ // ...
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+
+ AvoidHardcodingId
+ Avoid hardcoding id
+ category/apex/errorprone.xml/AvoidHardcodingId
+ MAJOR
+ 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 {
+ 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...
+ }
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ AvoidLogicInTrigger
+ Avoid logic in trigger
+ category/apex/bestpractices.xml/AvoidLogicInTrigger
+ MAJOR
+ 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).
]]>
+ pmd
+ bestpractices
+
+
+ AvoidNonExistentAnnotations
+ Avoid non existent annotations
+ category/apex/errorprone.xml/AvoidNonExistentAnnotations
+ MAJOR
+ 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 void methodWithNonExistentAnnotation() {
+ // ...
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ AvoidNonRestrictiveQueries
+ Avoid non restrictive queries
+ category/apex/performance.xml/AvoidNonRestrictiveQueries
+ MAJOR
+ 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 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
+ }
+ }
]]>
+ pmd
+ performance
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ AvoidStatefulDatabaseResult
+ Avoid stateful database result
+ category/apex/errorprone.xml/AvoidStatefulDatabaseResult
+ CRITICAL
+ 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
+ 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;
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ ClassNamingConventions
+ Class naming conventions
+ category/apex/codestyle.xml/ClassNamingConventions
+ MAJOR
+ 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 will be reported unless you change the regex
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ CognitiveComplexity
+ Cognitive complexity
+ category/apex/design.xml/CognitiveComplexity
+ MAJOR
+ 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
+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.
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ CyclomaticComplexity
+ Cyclomatic complexity
+ category/apex/design.xml/CyclomaticComplexity
+ MAJOR
+ 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
+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;
+ }
+ }
+ }
+ }
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ DebugsShouldUseLoggingLevel
+ Debugs should use logging level
+ category/apex/bestpractices.xml/DebugsShouldUseLoggingLevel
+ MAJOR
+ 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
+ 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
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+ strictMode
+
+ false
+ BOOLEAN
+
+
+
+ EagerlyLoadedDescribeSObjectResult
+ Eagerly loaded describe SObject result
+ category/apex/performance.xml/EagerlyLoadedDescribeSObjectResult
+ MAJOR
+ 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.
+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;
+ }
+ }
+ }
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
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ EmptyWhileStmt
+ Empty while stmt
+ category/apex/errorprone.xml/EmptyWhileStmt
+ MAJOR
+ 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) {
+ while (a == b) {
+ // empty!
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ ExcessiveClassLength
+ Excessive class length
+ category/apex/design.xml/ExcessiveClassLength
+ MAJOR
+ 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 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
+ }
+ }
]]>
+ pmd
+ design
+
+
+ ExcessiveParameterList
+ Excessive parameter List
+ category/apex/design.xml/ExcessiveParameterList
+ MAJOR
+ 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
+ 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) {
+ // ...
+ }
]]>
+ pmd
+ design
+
+
+ ExcessivePublicCount
+ Excessive public count
+ category/apex/design.xml/ExcessivePublicCount
+ MAJOR
+ 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 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 ...]
+ }
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ FieldDeclarationsShouldBeAtStart
+ Field declarations should be at start
+ category/apex/codestyle.xml/FieldDeclarationsShouldBeAtStart
+ MINOR
+ 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 {
+ public Integer someField; // good
+
+ public void someMethod() {
+ }
+
+ public Integer anotherField; // bad
+ }
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ FieldNamingConventions
+ Field naming conventions
+ category/apex/codestyle.xml/FieldNamingConventions
+ MAJOR
+ 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 {
+ Integer instanceField; // This is in camel case, so it's ok
+
+ Integer INSTANCE_FIELD; // This will be reported unless you change the regex
+ }
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ ForLoopsMustUseBraces
+ For loops must use braces
+ category/apex/codestyle.xml/ForLoopsMustUseBraces
+ MINOR
+ 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
+ foo();
+
+ for (int i = 0; i < 42; i++) { // preferred approach
+ foo();
+ }
]]>
+ pmd
+ codestyle
+
+
+ FormalParameterNamingConventions
+ Formal parameter naming conventions
+ category/apex/codestyle.xml/FormalParameterNamingConventions
+ MAJOR
+ 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 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
+ }
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ IfElseStmtsMustUseBraces
+ If else stmts must use braces
+ category/apex/codestyle.xml/IfElseStmtsMustUseBraces
+ MINOR
+ 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
+ if (foo) x++;
+
+ // but this is not
+ if (foo)
+ x = x+1;
+ else
+ x = x-1;
]]>
+ pmd
+ codestyle
+
+
+ IfStmtsMustUseBraces
+ If stmts must use braces
+ category/apex/codestyle.xml/IfStmtsMustUseBraces
+ MINOR
+ 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
+ x++;
+
+ if (foo) { // preferred approach
+ x++;
+ }
]]>
+ pmd
+ codestyle
+
+
+ InaccessibleAuraEnabledGetter
+ Inaccessible aura enabled getter
+ category/apex/errorprone.xml/InaccessibleAuraEnabledGetter
+ MAJOR
+ 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 {
+ @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;
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ LocalVariableNamingConventions
+ Local variable naming conventions
+ category/apex/codestyle.xml/LocalVariableNamingConventions
+ MAJOR
+ 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 Foo() {
+ Integer localVariable; // This is in camel case, so it's ok
+
+ Integer LOCAL_VARIABLE; // This will be reported unless you change the regex
+ }
+ }
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ MethodNamingConventions
+ Method naming conventions
+ category/apex/codestyle.xml/MethodNamingConventions
+ MAJOR
+ 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 void instanceMethod() { } // This is in camel case, so it's ok
+
+ public void INSTANCE_METHOD() { } // This will be reported unless you change the regex
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ MethodWithSameNameAsEnclosingClass
+ Method with same name as enclosing class
+ category/apex/errorprone.xml/MethodWithSameNameAsEnclosingClass
+ MAJOR
+ 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 {
+ // this is OK because it is a constructor
+ public MyClass() {}
+ // this is bad because it is a method
+ public void MyClass() {}
+ }
]]>
+ pmd
+ errorprone
+
+
+ NcssConstructorCount
+ NCSS constructor count
+ category/apex/design.xml/NcssConstructorCount
+ MAJOR
+ 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 {
+ //this constructor only has 1 NCSS lines
+ public Foo() {
+ super();
+
+
+
+
+ super.foo();
+ }
+ }
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ NcssMethodCount
+ NCSS method count
+ category/apex/design.xml/NcssMethodCount
+ MAJOR
+ 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 {
+ //this method only has 1 NCSS lines
+ public Integer method() {
+ super.method();
+
+
+
+ return 1;
+ }
+ }
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ NcssTypeCount
+ NCSS type count
+ category/apex/design.xml/NcssTypeCount
+ MAJOR
+ 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
+ public class Foo extends Bar {
+ public Foo() {
+ super();
+
+
+
+
+
+ super.foo();
+ }
+ }
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ OneDeclarationPerLine
+ One declaration per line
+ category/apex/codestyle.xml/OneDeclarationPerLine
+ MAJOR
+ 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; // ok by default, can be flagged setting the strictMode property
+
+ Integer a; // preferred approach
+ Integer b;
]]>
+ pmd
+ codestyle
+
+ strictMode
+
+ false
+ BOOLEAN
+
+
+ reportInForLoopInitializer
+
+ true
+ BOOLEAN
+
+
+
+ OperationWithHighCostInLoop
+ Operation with high cost in loop
+ category/apex/performance.xml/OperationWithHighCostInLoop
+ MAJOR
+ 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()
+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);
+ }
+ }
+ }
+ }
]]>
+ pmd
+ performance
+
+
+ OperationWithLimitsInLoop
+ Operation with limits in loop
+ category/apex/performance.xml/OperationWithLimitsInLoop
+ MAJOR
+ 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 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);
+ }
+ }
+ }
]]>
+ pmd
+ performance
+
+
+ OverrideBothEqualsAndHashcode
+ Override both equals and hashcode
+ category/apex/errorprone.xml/OverrideBothEqualsAndHashcode
+ MAJOR
+ 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.
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
+ }
+ }
]]>
+ pmd
+ errorprone
+
+
+ PropertyNamingConventions
+ Property naming conventions
+ category/apex/codestyle.xml/PropertyNamingConventions
+ MAJOR
+ 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 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
+ }
]]>
+ pmd
+ codestyle
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ QueueableWithoutFinalizer
+ Queueable without finalizer
+ category/apex/bestpractices.xml/QueueableWithoutFinalizer
+ MINOR
+ 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.
+ 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
+ }
+ }
+ }
]]>
+ pmd
+ bestpractices
+
+
+ StdCyclomaticComplexity
+ Std cyclomatic complexity
+ category/apex/design.xml/StdCyclomaticComplexity
+ MAJOR
+ 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
+ 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;
+ }
+ }
+ }
]]>
+ pmd
+ design
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ TestMethodsMustBeInTestClasses
+ Test methods must be in test classes
+ category/apex/errorprone.xml/TestMethodsMustBeInTestClasses
+ MAJOR
+ 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.
]]>
+ pmd
+ errorprone
+
+
+ TooManyFields
+ Too many fields
+ category/apex/design.xml/TooManyFields
+ MAJOR
+ 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 {
+ // 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;
+ }
]]>
+ pmd
+ design
+
+
+ TypeShadowsBuiltInNamespace
+ Type shadows built in namespace
+ category/apex/errorprone.xml/TypeShadowsBuiltInNamespace
+ BLOCKER
+ Title of issues: This name causes a collision with a class, enum, or interface used in the {0} 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.
+ 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';
+ }
+ }
]]>
+ pmd
+ errorprone
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ UnusedLocalVariable
+ Unused local variable
+ category/apex/bestpractices.xml/UnusedLocalVariable
+ MAJOR
+ 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) {
+ String x = 'some string'; // not used
+
+ String y = 'some other string'; // used in the next line
+ return z.equals(y);
+ }
]]>
+ pmd
+ bestpractices
+
+ violationSuppressRegex
+
+
+ STRING
+
+
+
+ UnusedMethod
+ Unused method
+ category/apex/design.xml/UnusedMethod
+ MAJOR
+ 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
+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:
+
]]>
+ pmd
+ design
+
+
+ WhileLoopsMustUseBraces
+ While loops must use braces
+ category/apex/codestyle.xml/WhileLoopsMustUseBraces
+ MINOR
+ 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
+ x++;
+
+ while (true) { // preferred approach
+ x++;
+ }
]]>
+ pmd
+ codestyle
+
+
\ No newline at end of file
diff --git a/sonar-pmd-apex-plugin/src/test/java/org/sonar/plugins/pmd/AbstractPmdExecutorTest.java b/sonar-pmd-apex-plugin/src/test/java/org/sonar/plugins/pmd/AbstractPmdExecutorTest.java
new file mode 100644
index 00000000..ebeb098d
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/test/java/org/sonar/plugins/pmd/AbstractPmdExecutorTest.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module tests
+ */
+package org.sonar.plugins.pmd;
+
+import net.sourceforge.pmd.lang.rule.RuleSetLoadException;
+import net.sourceforge.pmd.reporting.Report;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.rule.RuleScope;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public abstract class AbstractPmdExecutorTest {
+
+ protected final DefaultFileSystem fileSystem = new DefaultFileSystem(new File("."));
+ protected final ActiveRules activeRules = mock(ActiveRules.class);
+ protected final PmdConfiguration pmdConfiguration = mock(PmdConfiguration.class);
+ protected final PmdTemplate pmdTemplate = mock(PmdTemplate.class);
+ protected final MapSettings settings = new MapSettings();
+
+ protected AbstractPmdExecutor pmdExecutor;
+
+ protected static DefaultInputFile fileApex(String path, Type type) {
+ return TestInputFileBuilder.create("", path)
+ .setType(type)
+ .setLanguage(PmdConstants.LANGUAGE_APEX_KEY)
+ .build();
+ }
+
+ @BeforeEach
+ void setUpAbstractTest() {
+ fileSystem.setEncoding(StandardCharsets.UTF_8);
+ settings.setProperty(PmdConstants.JAVA_SOURCE_VERSION, "1.8");
+ }
+
+ @Test
+ void whenNoFilesToAnalyzeThenExecutionSucceedsWithBlankReport() {
+ final Report result = pmdExecutor.execute();
+ assertThat(result).isNotNull();
+ assertThat(result.getViolations()).isEmpty();
+ assertThat(result.getProcessingErrors()).isEmpty();
+ }
+
+ @Test
+ void unknown_pmd_ruleset() {
+ when(pmdConfiguration.dumpXmlRuleSet(anyString(), anyString(), ArgumentMatchers.any(RuleScope.class))).thenReturn(new File("unknown"));
+
+ DefaultInputFile srcFile = getAppropriateInputFileForTest();
+ fileSystem.add(srcFile);
+
+ final Throwable thrown = catchThrowable(() -> pmdExecutor.execute());
+
+ assertThat(thrown)
+ .isInstanceOf(IllegalStateException.class)
+ .hasCauseInstanceOf(RuleSetLoadException.class);
+ }
+
+ protected abstract DefaultInputFile getAppropriateInputFileForTest();
+
+ protected void setupPmdRuleSet(String repositoryKey, String profileFileName) {
+ final Path sourcePath = Paths.get("src/test/resources/org/sonar/plugins/pmd/").resolve(profileFileName);
+ when(pmdConfiguration.dumpXmlRuleSet(eq(repositoryKey), anyString(), ArgumentMatchers.any(RuleScope.class))).thenReturn(sourcePath.toFile());
+ }
+}
diff --git a/sonar-pmd-apex-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java b/sonar-pmd-apex-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java
new file mode 100644
index 00000000..f75bc3d0
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/test/java/org/sonar/plugins/pmd/PmdApexExecutorTest.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube PMD7 Plugin - Apex module tests
+ */
+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));
+ }
+}
diff --git a/sonar-pmd-apex-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml b/sonar-pmd-apex-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml
new file mode 100644
index 00000000..a48fc460
--- /dev/null
+++ b/sonar-pmd-apex-plugin/src/test/resources/org/sonar/plugins/pmd/simple-apex.xml
@@ -0,0 +1,7 @@
+
+
+ Sonar PMD apex rules
+
+ 2
+
+
diff --git a/sonar-pmd-lib/pom.xml b/sonar-pmd-lib/pom.xml
index 5f6b7a88..0745fb22 100644
--- a/sonar-pmd-lib/pom.xml
+++ b/sonar-pmd-lib/pom.xml
@@ -58,11 +58,15 @@
- org.jetbrains
- annotations
- 26.0.2-1
+ org.jspecify
+ jspecifycompile
+
+ org.jdom
+ jdom2
+ ${jdom2.version}
+ net.sourceforge.pmdpmd-java
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/AbstractPmdExecutor.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/AbstractPmdExecutor.java
similarity index 73%
rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/AbstractPmdExecutor.java
rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/AbstractPmdExecutor.java
index 3136ebc4..8064111f 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/AbstractPmdExecutor.java
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/AbstractPmdExecutor.java
@@ -1,21 +1,5 @@
/*
- * 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.
+ * Shared abstract PMD executor moved to lib to be reused by plugins.
*/
package org.sonar.plugins.pmd;
@@ -27,8 +11,9 @@
import net.sourceforge.pmd.reporting.FileAnalysisListener;
import net.sourceforge.pmd.reporting.Report;
import net.sourceforge.pmd.util.log.PmdReporter;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.jetbrains.annotations.NotNull;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
@@ -41,20 +26,15 @@
import org.sonar.api.rule.RuleScope;
import org.sonar.plugins.pmd.xml.PmdRuleSet;
import org.sonar.plugins.pmd.xml.PmdRuleSets;
-import org.sonar.plugins.pmd.xml.factory.RuleSetFactory;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.net.URLClassLoader;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
-/**
- * Abstract base class for PMD executors that contains common functionality.
- */
public abstract class AbstractPmdExecutor {
protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractPmdExecutor.class);
@@ -76,10 +56,6 @@ protected static void accept(FileAnalysisListener fal) {
LOGGER.debug("Got FileAnalysisListener: {}", fal);
}
- /**
- * Execute PMD analysis
- * @return The PMD report containing the results of the analysis
- */
public Report execute() {
final long startTimeMs = System.currentTimeMillis();
LOGGER.info(getStartMessage(), PMDVersion.VERSION);
@@ -99,35 +75,35 @@ public Report execute() {
return null;
}
- /**
- * Get the start message for logging
- * @return The start message
- */
protected abstract String getStartMessage();
- /**
- * Get the end message for logging
- * @return The end message
- */
protected abstract String getEndMessage();
- /**
- * Create a classloader for PMD analysis
- * @return The classloader
- */
+ protected Report executeLanguage(URLClassLoader classLoader, String languageKey, String repositoryKey) {
+ final PmdTemplate pmdFactory = createPmdTemplate(classLoader);
+ final Optional mainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, languageKey), repositoryKey, RuleScope.MAIN);
+ final Optional testReport = executeRules(pmdFactory, hasFiles(Type.TEST, languageKey), repositoryKey, RuleScope.TEST);
+
+ if (LOGGER.isDebugEnabled()) {
+ mainReport.ifPresent(this::writeDebugLine);
+ testReport.ifPresent(this::writeDebugLine);
+ }
+
+ Consumer fileAnalysisListenerConsumer = AbstractPmdExecutor::accept;
+
+ Report unionReport = Report.buildReport(fileAnalysisListenerConsumer);
+ unionReport = mainReport.map(unionReport::union).orElse(unionReport);
+ unionReport = testReport.map(unionReport::union).orElse(unionReport);
+
+ pmdConfiguration.dumpXmlReport(unionReport);
+
+ return unionReport;
+ }
+
protected abstract URLClassLoader createClassloader();
- /**
- * Execute PMD analysis with the given classloader
- * @param classLoader The classloader to use
- * @return The PMD report
- */
protected abstract Report executePmd(URLClassLoader classLoader);
- /**
- * Write debug information about the report
- * @param r The report
- */
protected void writeDebugLine(Report r) {
LOGGER.debug("Report (violations, suppressedViolations, processingErrors, configurationErrors): {}, {}, {}, {}", r.getViolations().size(), r.getSuppressedViolations().size(), r.getProcessingErrors().size(), r.getConfigurationErrors().size());
if (!r.getViolations().isEmpty()) {
@@ -144,12 +120,6 @@ protected void writeDebugLine(Report r) {
}
}
- /**
- * Get files of the given type and language
- * @param fileType The file type (MAIN or TEST)
- * @param languageKey The language key
- * @return The files
- */
protected Iterable hasFiles(Type fileType, String languageKey) {
final FilePredicates predicates = fs.predicates();
return fs.inputFiles(
@@ -160,16 +130,8 @@ protected Iterable hasFiles(Type fileType, String languageKey) {
);
}
- /**
- * Execute PMD rules on the given files
- * @param pmdFactory The PMD template
- * @param files The files to analyze
- * @param repositoryKey The repository key
- * @return The report
- */
protected Optional executeRules(PmdTemplate pmdFactory, Iterable files, String repositoryKey, RuleScope scope) {
if (!files.iterator().hasNext()) {
- // Nothing to analyze
LOGGER.debug("No files to analyze for {}", repositoryKey);
return Optional.empty();
}
@@ -177,7 +139,6 @@ protected Optional executeRules(PmdTemplate pmdFactory, Iterable executeRules(PmdTemplate pmdFactory, Iterablesonar.pmd.generateXml is set to false.
- *
- * @param report The report which shall be written into an XML file.
- * @return The file reference to the XML document.
- */
Path dumpXmlReport(Report report) {
if (!settings.getBoolean(PROPERTY_GENERATE_XML).orElse(false)) {
return null;
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdConstants.java
similarity index 88%
rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java
rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdConstants.java
index 35728eec..53cb7dab 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdConstants.java
+++ b/sonar-pmd-lib/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 REPOSITORY_NAME = "PMD";
+ public static final String MAIN_APEX_REPOSITORY_KEY = "pmd-apex";
+ public static final String REPOSITORY_JAVA_NAME = "PMD";
public static final String REPOSITORY_KOTLIN_NAME = "PMD";
+ public static final String REPOSITORY_APEX_NAME = "PMD";
public static final String XPATH_CLASS = "net.sourceforge.pmd.lang.rule.xpath.XPathRule";
public static final String XPATH_EXPRESSION_PARAM = "xpath";
@@ -58,6 +60,8 @@ 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";
+ public static final String LANGUAGE_APEX_NAME = "Apex";
private PmdConstants() {
}
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPriorities.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdPriorities.java
similarity index 56%
rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPriorities.java
rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdPriorities.java
index 3005cdb3..8db850fe 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdPriorities.java
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdPriorities.java
@@ -1,29 +1,13 @@
/*
- * SonarQube PMD 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.
+ * Shared PmdPriorities moved to lib to reduce duplication across modules.
*/
package org.sonar.plugins.pmd;
import java.util.Locale;
import java.util.Objects;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.sonar.api.batch.rule.ActiveRule;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RulePriority;
@@ -36,7 +20,7 @@ private PmdPriorities() {
// only static methods
}
- public static org.sonar.api.rules.RulePriority sonarPrioOf(@Nonnull PmdRule pmdRule) {
+ public static org.sonar.api.rules.RulePriority sonarPrioOf(@NonNull PmdRule pmdRule) {
return toSonarPrio(pmdRule.getPriority());
}
@@ -57,11 +41,11 @@ public static Integer fromSonarPrio(org.sonar.api.rules.RulePriority priority) {
return Math.abs(priority.ordinal() - NUM_SEVERITIES);
}
- public static Integer fromSonarSeverity(@Nonnull String severity) {
+ public static Integer fromSonarSeverity(@NonNull String severity) {
return Math.abs(NUM_SEVERITIES - Severity.ALL.indexOf(severity));
}
- public static Integer ofSonarRule(@Nonnull ActiveRule sonarRule) {
+ public static Integer ofSonarRule(@NonNull ActiveRule sonarRule) {
return fromSonarSeverity(sonarRule.severity().toUpperCase(Locale.ENGLISH));
}
}
diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java
new file mode 100644
index 00000000..1e6c5b6c
--- /dev/null
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java
@@ -0,0 +1,174 @@
+/*
+ * Shared PMD template moved to lib to be reused by plugins.
+ */
+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.java.JavaLanguageModule;
+import net.sourceforge.pmd.lang.rule.RuleSet;
+import net.sourceforge.pmd.renderers.EmptyRenderer;
+import net.sourceforge.pmd.reporting.Report;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.fs.InputFile;
+
+import java.nio.charset.Charset;
+import java.nio.file.Paths;
+import java.util.*;
+
+public class PmdTemplate {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PmdTemplate.class);
+ private static final Map JAVA_VERSIONS = prepareVersions();
+
+ private static Map prepareVersions() {
+ final Map versions = new HashMap<>();
+ versions.put("8", "1.8");
+ versions.put("1.9", "9");
+ versions.put("1.10", "10");
+ versions.put("1.11", "11");
+ versions.put("1.12", "12");
+ versions.put("1.13", "13");
+ versions.put("1.14", "14");
+ versions.put("1.15", "15");
+ versions.put("1.16", "16");
+ versions.put("1.17", "17");
+ versions.put("1.18", "18");
+ versions.put("1.19", "19");
+ versions.put("1.20", "20");
+ versions.put("1.21", "21");
+ versions.put("1.22", "22");
+ versions.put("1.23", "23");
+ versions.put("1.24", "24");
+ versions.put("1.25", "25");
+ versions.put("1.25-preview", "25-preview");
+ return versions;
+ }
+
+ private final PMDConfiguration configuration;
+
+ PmdTemplate(PMDConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ public static PmdTemplate create(String javaVersion, ClassLoader classloader, Charset charset) {
+ PMDConfiguration configuration = new PMDConfiguration();
+
+ 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);
+ configuration.setIgnoreIncrementalAnalysis(true);
+ configuration.setReportFormat(EmptyRenderer.NAME);
+
+ return new PmdTemplate(configuration);
+ }
+
+ 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)) {
+ Language apex = LanguageRegistry.PMD.getLanguageById("apex");
+ if (apex != null) {
+ LanguageVersion languageVersion = apex.getDefaultVersion();
+ LOG.info("Using Apex default version");
+ return languageVersion;
+ }
+ LOG.warn("Apex language module not found on classpath; falling back to Java default version");
+ return new JavaLanguageModule().getDefaultVersion();
+ } else if (PmdConstants.LANGUAGE_KOTLIN_KEY.equals(languageKey)) {
+ Language kotlin = LanguageRegistry.PMD.getLanguageById("kotlin");
+ if (kotlin != null) {
+ LanguageVersion languageVersion = kotlin.getDefaultVersion();
+ LOG.info("Using Kotlin default version");
+ return languageVersion;
+ }
+ LOG.warn("Kotlin language module not found on classpath; falling back to Java default version");
+ return new JavaLanguageModule().getDefaultVersion();
+ }
+
+ return new JavaLanguageModule().getDefaultVersion();
+ }
+
+ private static String normalize(String version) {
+ return JAVA_VERSIONS.getOrDefault(version, version);
+ }
+
+ PMDConfiguration configuration() {
+ return configuration;
+ }
+
+ public Report process(Iterable files, RuleSet ruleset) {
+ try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {
+ pmd.addRuleSet(ruleset);
+
+ Map> filesByLanguage = new HashMap<>();
+ for (InputFile file : files) {
+ String language = file.language();
+ if (language == null) {
+ 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 {
+ language = PmdConstants.LANGUAGE_JAVA_KEY;
+ }
+ }
+
+ filesByLanguage.computeIfAbsent(language, k -> new ArrayList<>()).add(file);
+ }
+
+ 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);
+
+ if (PmdConstants.LANGUAGE_APEX_KEY.equals(language)) {
+ Language apex = LanguageRegistry.PMD.getLanguageById("apex");
+ if (apex != null) {
+ LanguageVersion apexVersion = apex.getDefaultVersion();
+ configuration.setDefaultLanguageVersion(apexVersion);
+ LOG.info("Set language version to Apex: {}", apexVersion.getName());
+ } else {
+ LOG.warn("Apex language module not found on classpath; keeping current default version");
+ }
+ } else if (PmdConstants.LANGUAGE_KOTLIN_KEY.equals(language)) {
+ Language kotlin = LanguageRegistry.PMD.getLanguageById("kotlin");
+ if (kotlin != null) {
+ LanguageVersion kotlinVersion = kotlin.getDefaultVersion();
+ configuration.setDefaultLanguageVersion(kotlinVersion);
+ LOG.info("Set language version to Kotlin: {}", kotlinVersion.getName());
+ } else {
+ LOG.warn("Kotlin language module not found on classpath; keeping current default version");
+ }
+ } else {
+ LanguageVersion javaVersion = languageVersion(PmdConstants.LANGUAGE_JAVA_KEY, PmdConstants.JAVA_SOURCE_VERSION_DEFAULT_VALUE);
+ configuration.setDefaultLanguageVersion(javaVersion);
+ LOG.info("Set language version to Java: {}", javaVersion.getName());
+ }
+
+ 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/PmdViolationRecorder.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java
similarity index 72%
rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java
rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java
index d802cf71..bc878183 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/PmdViolationRecorder.java
@@ -1,21 +1,5 @@
/*
- * 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.
+ * Shared violation recorder moved to lib to be reused by plugins.
*/
package org.sonar.plugins.pmd;
@@ -31,14 +15,13 @@
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.rule.RuleKey;
-import org.sonar.plugins.pmd.rule.PmdKotlinRulesDefinition;
import java.util.Optional;
@ScannerSide
public class PmdViolationRecorder {
- private static final Logger LOGGER = LoggerFactory.getLogger(PmdKotlinRulesDefinition.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(PmdViolationRecorder.class);
private final FileSystem fs;
private final ActiveRules activeRules;
@@ -57,7 +40,6 @@ public void saveViolation(RuleViolation pmdViolation, SensorContext context) {
LOGGER.trace("Found violation input file: {}", inputFile);
if (inputFile == null) {
- // Save violations only for existing resources
return;
}
@@ -66,7 +48,6 @@ public void saveViolation(RuleViolation pmdViolation, SensorContext context) {
LOGGER.trace("Found violation rule key: {}", ruleKey);
if (ruleKey == null) {
- // Save violations only for enabled rules
return;
}
@@ -103,7 +84,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-lib/src/main/java/org/sonar/plugins/pmd/TextRangeCalculator.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/TextRangeCalculator.java
new file mode 100644
index 00000000..35e5682b
--- /dev/null
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/TextRangeCalculator.java
@@ -0,0 +1,34 @@
+/*
+ * Shared TextRangeCalculator moved to lib.
+ */
+package org.sonar.plugins.pmd;
+
+import net.sourceforge.pmd.reporting.RuleViolation;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextPointer;
+import org.sonar.api.batch.fs.TextRange;
+
+class TextRangeCalculator {
+
+ private TextRangeCalculator() {
+ }
+
+ static TextRange calculate(RuleViolation pmdViolation, InputFile inputFile) {
+ final int startLine = calculateBeginLine(pmdViolation);
+ final int endLine = calculateEndLine(pmdViolation);
+
+ final TextPointer startPointer = inputFile.selectLine(startLine).start();
+ final TextPointer endPointer = inputFile.selectLine(endLine).end();
+
+ return inputFile.newRange(startPointer, endPointer);
+ }
+
+ private static int calculateEndLine(RuleViolation pmdViolation) {
+ return Math.max(pmdViolation.getBeginLine(), pmdViolation.getEndLine());
+ }
+
+ private static int calculateBeginLine(RuleViolation pmdViolation) {
+ int minLine = Math.min(pmdViolation.getBeginLine(), pmdViolation.getEndLine());
+ return minLine > 0 ? minLine : calculateEndLine(pmdViolation);
+ }
+}
diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/JavaRulePropertyExtractor.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/JavaRulePropertyExtractor.java
index fa5cb640..8e40ef88 100644
--- a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/JavaRulePropertyExtractor.java
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/JavaRulePropertyExtractor.java
@@ -4,7 +4,8 @@
import net.sourceforge.pmd.properties.PropertyConstraint;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertySource;
-import org.jetbrains.annotations.NotNull;
+
+import org.jspecify.annotations.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.plugins.pmd.rule.util.ZipBombProtection;
@@ -219,7 +220,7 @@ private String resolvePropertyType(PropertyDescriptor> propertyDescriptor) {
* @param o The object to get the simplified type name from
* @return The simplified class name as a String
*/
- private static @NotNull String convertKnownTypes(Object o) {
+ private static @NonNull String convertKnownTypes(Object o) {
String simpleName = o.getClass().getSimpleName();
// is this needed? there is only: %%% found simplename with Empty: EmptySet
if (simpleName.startsWith("Empty")) {
diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/RulesDefinitionXmlLoader.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/RulesDefinitionXmlLoader.java
index bb82ffa1..1d7ce64f 100644
--- a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/RulesDefinitionXmlLoader.java
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/RulesDefinitionXmlLoader.java
@@ -25,7 +25,8 @@
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang3.StringUtils;
-import org.jetbrains.annotations.Nullable;
+
+import org.jspecify.annotations.Nullable;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.rule.RuleScope;
import org.sonar.api.rule.RuleStatus;
diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/util/RuleParamFormatter.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/util/RuleParamFormatter.java
index 5da76e77..e8f89a21 100644
--- a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/util/RuleParamFormatter.java
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/rule/util/RuleParamFormatter.java
@@ -19,7 +19,7 @@
*/
package org.sonar.plugins.pmd.rule.util;
-import org.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.Nullable;
import java.util.List;
import java.util.Objects;
diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/util/ClassLoaderUtils.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/util/ClassLoaderUtils.java
new file mode 100644
index 00000000..2376b573
--- /dev/null
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/util/ClassLoaderUtils.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Utility methods to construct URLClassLoaders in a consistent and safe way.
+ * Located in sonar-pmd-lib for reuse by both the core plugin and extension examples.
+ */
+public final class ClassLoaderUtils {
+
+ private ClassLoaderUtils() {
+ // utility class
+ }
+
+ /**
+ * Create an URLClassLoader from a collection of classpath elements (directories or jars).
+ *
+ * @param classpathElements Collection of files to be added to the classloader
+ * @return URLClassLoader containing all provided elements
+ * @throws IllegalStateException if any classpath element cannot be converted to a URL
+ */
+ public static URLClassLoader fromClasspath(Collection classpathElements) {
+ List urls = new ArrayList<>();
+ if (classpathElements != null) {
+ 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);
+ }
+ }
+ }
+ return new URLClassLoader(urls.toArray(new URL[0]));
+ }
+
+ /**
+ * Create an empty URLClassLoader.
+ */
+ public static URLClassLoader empty() {
+ return new URLClassLoader(new URL[0]);
+ }
+}
diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/util/PluginExtensions.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/util/PluginExtensions.java
new file mode 100644
index 00000000..84a2ddcd
--- /dev/null
+++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/util/PluginExtensions.java
@@ -0,0 +1,68 @@
+/*
+ * 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.util;
+
+import org.sonar.api.Plugin;
+import org.sonar.api.config.PropertyDefinition;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility methods to help registering extensions and common properties for Sonar-PMD plugins.
+ *
+ * This class lives in sonar-pmd-lib so both the main plugin and the integration-test extension plugin
+ * can reuse the same utility and keep their Plugin implementations concise.
+ */
+public final class PluginExtensions {
+
+ private PluginExtensions() {
+ // utility
+ }
+
+ /**
+ * Adds the given extensions to the plugin context.
+ *
+ * @param context The SonarQube plugin context
+ * @param extensions The extensions to add (classes or instances)
+ */
+ public static void addExtensions(Plugin.Context context, Object... extensions) {
+ if (extensions == null || extensions.length == 0) {
+ return;
+ }
+ Listorg.sonarsource.pmd
@@ -173,14 +174,6 @@
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
-
- -Xmx512m
-
- org.sonarsource.sonar-packaging-maven-pluginsonar-packaging-maven-plugin
@@ -205,7 +198,7 @@
- 30000000
+ 6000000012000000${project.build.directory}/${project.build.finalName}.jar
@@ -230,7 +223,6 @@
generate-pmd-rules
-
org.codehaus.gmavengroovy-maven-plugin
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdJavaExecutor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdJavaExecutor.java
index 54418dff..4f898aa4 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdJavaExecutor.java
+++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdJavaExecutor.java
@@ -19,24 +19,16 @@
*/
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 org.sonar.api.rule.RuleScope;
+import org.sonar.plugins.pmd.util.ClassLoaderUtils;
import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
import java.net.URLClassLoader;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Consumer;
/**
* PMD executor for Java files.
@@ -64,24 +56,7 @@ protected String getEndMessage() {
@Override
protected 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, RuleScope.MAIN);
- final Optional javaTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_JAVA_KEY), PmdConstants.MAIN_JAVA_REPOSITORY_KEY, RuleScope.TEST);
-
- if (LOGGER.isDebugEnabled()) {
- javaMainReport.ifPresent(this::writeDebugLine);
- javaTestReport.ifPresent(this::writeDebugLine);
- }
-
- Consumer fileAnalysisListenerConsumer = AbstractPmdExecutor::accept;
-
- Report unionReport = Report.buildReport(fileAnalysisListenerConsumer);
- unionReport = javaMainReport.map(unionReport::union).orElse(unionReport);
- unionReport = javaTestReport.map(unionReport::union).orElse(unionReport);
-
- pmdConfiguration.dumpXmlReport(unionReport);
-
- return unionReport;
+ return executeLanguage(classLoader, PmdConstants.LANGUAGE_JAVA_KEY, PmdConstants.MAIN_JAVA_REPOSITORY_KEY);
}
/**
@@ -90,14 +65,6 @@ protected Report executePmd(URLClassLoader classLoader) {
@Override
protected URLClassLoader createClassloader() {
Collection classpathElements = classpathProvider.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);
- }
- }
- return new URLClassLoader(urls.toArray(new URL[0]));
+ return ClassLoaderUtils.fromClasspath(classpathElements);
}
}
\ No newline at end of file
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdKotlinExecutor.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdKotlinExecutor.java
index 143e3224..84267eba 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdKotlinExecutor.java
+++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdKotlinExecutor.java
@@ -19,19 +19,13 @@
*/
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 org.sonar.api.rule.RuleScope;
-import java.net.URL;
import java.net.URLClassLoader;
-import java.util.Optional;
-import java.util.function.Consumer;
/**
* PMD executor for Kotlin files.
@@ -57,24 +51,7 @@ protected String getEndMessage() {
@Override
protected Report executePmd(URLClassLoader classLoader) {
- final PmdTemplate pmdFactory = createPmdTemplate(classLoader);
- final Optional kotlinMainReport = executeRules(pmdFactory, hasFiles(Type.MAIN, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY, RuleScope.MAIN);
- final Optional kotlinTestReport = executeRules(pmdFactory, hasFiles(Type.TEST, PmdConstants.LANGUAGE_KOTLIN_KEY), PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY, RuleScope.TEST);
-
- if (LOGGER.isDebugEnabled()) {
- kotlinMainReport.ifPresent(this::writeDebugLine);
- kotlinTestReport.ifPresent(this::writeDebugLine);
- }
-
- Consumer fileAnalysisListenerConsumer = AbstractPmdExecutor::accept;
-
- Report unionReport = Report.buildReport(fileAnalysisListenerConsumer);
- unionReport = kotlinMainReport.map(unionReport::union).orElse(unionReport);
- unionReport = kotlinTestReport.map(unionReport::union).orElse(unionReport);
-
- pmdConfiguration.dumpXmlReport(unionReport);
-
- return unionReport;
+ return executeLanguage(classLoader, PmdConstants.LANGUAGE_KOTLIN_KEY, PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY);
}
/**
@@ -84,6 +61,6 @@ protected Report executePmd(URLClassLoader classLoader) {
@Override
protected URLClassLoader createClassloader() {
// Create an empty URLClassLoader
- return new URLClassLoader(new URL[0]);
+ return org.sonar.plugins.pmd.util.ClassLoaderUtils.empty();
}
}
\ No newline at end of file
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 7a2b45e6..fe54f717 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
@@ -35,7 +35,7 @@ public class PmdSensor implements Sensor {
private final PmdViolationRecorder pmdViolationRecorder;
private final FileSystem fs;
- public PmdSensor(ActiveRules profile, PmdJavaExecutor javaExecutor, PmdKotlinExecutor kotlinExecutor,
+ public PmdSensor(ActiveRules profile, PmdJavaExecutor javaExecutor, PmdKotlinExecutor kotlinExecutor,
PmdViolationRecorder pmdViolationRecorder, FileSystem fs) {
this.profile = profile;
this.javaExecutor = javaExecutor;
@@ -47,9 +47,8 @@ public PmdSensor(ActiveRules profile, PmdJavaExecutor javaExecutor, PmdKotlinExe
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.TEST, 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));
}
private boolean hasFilesToCheck(Type type, String repositoryKey, String languageKey) {
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
deleted file mode 100644
index fcab56ea..00000000
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/PmdTemplate.java
+++ /dev/null
@@ -1,110 +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 net.sourceforge.pmd.*;
-import net.sourceforge.pmd.lang.LanguageVersion;
-import net.sourceforge.pmd.lang.java.JavaLanguageModule;
-import net.sourceforge.pmd.lang.rule.RuleSet;
-import net.sourceforge.pmd.renderers.EmptyRenderer;
-import net.sourceforge.pmd.reporting.Report;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.batch.fs.InputFile;
-
-import java.nio.charset.Charset;
-import java.nio.file.Paths;
-import java.util.*;
-
-public class PmdTemplate {
-
- private static final Logger LOG = LoggerFactory.getLogger(PmdTemplate.class);
- private static final Map JAVA_VERSIONS = prepareVersions();
-
- private static Map prepareVersions() {
- final Map versions = new HashMap<>();
- versions.put("8", "1.8");
- versions.put("1.9", "9");
- versions.put("1.10", "10");
- versions.put("1.11", "11");
- versions.put("1.12", "12");
- versions.put("1.13", "13");
- versions.put("1.14", "14");
- versions.put("1.15", "15");
- versions.put("1.16", "16");
- versions.put("1.17", "17");
- versions.put("1.18", "18");
- versions.put("1.19", "19");
- versions.put("1.20", "20");
- versions.put("1.21", "21");
- versions.put("1.22", "22");
- versions.put("1.23", "23");
- versions.put("1.24", "24");
- versions.put("1.25", "25");
- versions.put("1.25-preview", "25-preview");
- return versions;
- }
-
- private final PMDConfiguration configuration;
-
- PmdTemplate(PMDConfiguration configuration) {
- this.configuration = configuration;
- }
-
- public static PmdTemplate create(String javaVersion, ClassLoader classloader, Charset charset) {
- PMDConfiguration configuration = new PMDConfiguration();
- configuration.setDefaultLanguageVersion(languageVersion(javaVersion));
- configuration.setClassLoader(classloader);
- configuration.setSourceEncoding(charset);
- configuration.setFailOnViolation(false);
- configuration.setIgnoreIncrementalAnalysis(true);
- configuration.setReportFormat(EmptyRenderer.NAME);
-
- 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);
- }
- LOG.info("Java version: {}", version);
- return languageVersion;
- }
-
- private static String normalize(String version) {
- return JAVA_VERSIONS.getOrDefault(version, version);
- }
-
- PMDConfiguration configuration() {
- return 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()));
- }
- return pmd.performAnalysisAndCollectReport();
- }
- }
-}
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/TextRangeCalculator.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/TextRangeCalculator.java
deleted file mode 100644
index 570c1910..00000000
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/TextRangeCalculator.java
+++ /dev/null
@@ -1,68 +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 net.sourceforge.pmd.reporting.RuleViolation;
-import org.sonar.api.batch.fs.InputFile;
-import org.sonar.api.batch.fs.TextPointer;
-import org.sonar.api.batch.fs.TextRange;
-
-/**
- * Calculates a {@link org.sonar.api.batch.fs.TextRange} for a given {@link net.sourceforge.pmd.reporting.RuleViolation}.
- */
-class TextRangeCalculator {
-
- private TextRangeCalculator() {
- }
-
- static TextRange calculate(RuleViolation pmdViolation, InputFile inputFile) {
- final int startLine = calculateBeginLine(pmdViolation);
- final int endLine = calculateEndLine(pmdViolation);
-
- // PMD counts TABs differently, so we can not use RuleViolation#getBeginColumn and RuleViolation#getEndColumn
- // Therefore, we select complete lines.
- final TextPointer startPointer = inputFile.selectLine(startLine).start();
- final TextPointer endPointer = inputFile.selectLine(endLine).end();
-
- return inputFile.newRange(startPointer, endPointer);
- }
-
- /**
- * Calculates the endLIne of a violation report.
- *
- * @param pmdViolation The violation for which the endLine should be calculated.
- * @return The endLine is assumed to be the line with the biggest number.
- */
- private static int calculateEndLine(RuleViolation pmdViolation) {
- return Math.max(pmdViolation.getBeginLine(), pmdViolation.getEndLine());
- }
-
- /**
- * Calculates the beginLine of a violation report.
- *
- * @param pmdViolation The violation for which the beginLine should be calculated.
- * @return The beginLine is assumed to be the line with the smallest number. However, if the smallest number is
- * out-of-range (non-positive), it takes the other number.
- */
- private static int calculateBeginLine(RuleViolation pmdViolation) {
- int minLine = Math.min(pmdViolation.getBeginLine(), pmdViolation.getEndLine());
- return minLine > 0 ? minLine : calculateEndLine(pmdViolation);
- }
-}
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdRulesDefinition.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdRulesDefinition.java
index dce5e640..2fb9069c 100644
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdRulesDefinition.java
+++ b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/rule/PmdRulesDefinition.java
@@ -56,7 +56,7 @@ static void extractRulesData(NewRepository repository, String xmlRulesFilePath)
public void define(Context context) {
NewRepository repository = context
.createRepository(PmdConstants.MAIN_JAVA_REPOSITORY_KEY, PmdConstants.LANGUAGE_JAVA_KEY)
- .setName(PmdConstants.REPOSITORY_NAME);
+ .setName(PmdConstants.REPOSITORY_JAVA_NAME);
extractRulesData(repository, "/org/sonar/plugins/pmd/rules-java.xml");
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdProperty.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdProperty.java
deleted file mode 100644
index 42cdae75..00000000
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdProperty.java
+++ /dev/null
@@ -1,56 +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.xml;
-
-public class PmdProperty {
-
- private String name;
- private String value;
- private String cdataValue;
-
- public PmdProperty(String name, String value) {
- this.name = name;
- this.value = value;
- }
-
- public String getName() {
- return name;
- }
-
- public String getValue() {
- return value;
- }
-
- public void setValue(String value) {
- this.value = value;
- }
-
- public String getCdataValue() {
- return cdataValue;
- }
-
- public boolean isCdataValue() {
- return cdataValue != null;
- }
-
- public void setCdataValue(String cdataValue) {
- this.cdataValue = cdataValue;
- }
-}
diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java b/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java
deleted file mode 100644
index 7fb5c1bf..00000000
--- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java
+++ /dev/null
@@ -1,35 +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.xml.factory;
-
-import java.io.Closeable;
-
-import org.sonar.plugins.pmd.xml.PmdRuleSet;
-
-/**
- * Interface for all RuleSetFactories.
- */
-public interface RuleSetFactory extends Closeable {
-
- /**
- * @return A PMD Ruleset.
- */
- PmdRuleSet create();
-}
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..ecab90d7
--- /dev/null
+++ b/sonar-pmd-plugin/src/main/resources/com/sonar/sqale/pmd-model-apex.xml
@@ -0,0 +1 @@
+
diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdConstantsTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdConstantsTest.java
index c80ab21b..8127f7f8 100644
--- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdConstantsTest.java
+++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdConstantsTest.java
@@ -31,7 +31,7 @@ void checkDefinedKeys() {
assertThat(PmdConstants.PLUGIN_KEY).isEqualTo("pmd");
assertThat(PmdConstants.MAIN_JAVA_REPOSITORY_KEY).isEqualTo("pmd");
assertThat(PmdConstants.MAIN_KOTLIN_REPOSITORY_KEY).isEqualTo("pmd-kotlin");
- assertThat(PmdConstants.REPOSITORY_NAME).isEqualTo("PMD");
+ assertThat(PmdConstants.REPOSITORY_JAVA_NAME).isEqualTo("PMD");
assertThat(PmdConstants.XPATH_CLASS).isEqualTo("net.sourceforge.pmd.lang.rule.xpath.XPathRule");
assertThat(PmdConstants.XPATH_EXPRESSION_PARAM).isEqualTo("xpath");
assertThat(PmdConstants.XPATH_MESSAGE_PARAM).isEqualTo("message");
diff --git a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdRulesDefinitionTest.java b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdRulesDefinitionTest.java
index ae284ad4..92382553 100644
--- a/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdRulesDefinitionTest.java
+++ b/sonar-pmd-plugin/src/test/java/org/sonar/plugins/pmd/PmdRulesDefinitionTest.java
@@ -38,7 +38,7 @@ void test() {
definition.define(context);
RulesDefinition.Repository repository = context.repository(PmdConstants.MAIN_JAVA_REPOSITORY_KEY);
- assertThat(repository.name()).isEqualTo(PmdConstants.REPOSITORY_NAME);
+ assertThat(repository.name()).isEqualTo(PmdConstants.REPOSITORY_JAVA_NAME);
assertThat(repository.language()).isEqualTo(PmdConstants.LANGUAGE_JAVA_KEY);
List rules = repository.rules();
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 cebfd4a3..b37e0a96 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
@@ -265,6 +265,12 @@ void pmdSensorShouldNotRethrowKotlinExecutorExceptions() {
.isEqualTo(expectedException);
}
+
+
+
+
+
+
@Test
void should_to_string() {
final String toString = pmdSensor.toString();
@@ -300,6 +306,7 @@ private void mockKotlinExecutorResult(RuleViolation... violations) {
.thenReturn(createReport(violations));
}
+
private Report createReport(RuleViolation... violations) {
Consumer fileAnalysisListenerConsumer = fal -> {
for (RuleViolation violation : violations) {
@@ -337,4 +344,5 @@ private void addOneKotlinFile(Type type) {
.build()
);
}
+
}
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();