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.orchestrator sonar-orchestrator + test org.sonarsource.orchestrator sonar-orchestrator-junit4 + test org.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.maven sonar-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.maven sonar-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 + DoNotExtendIOException MAJOR + pmd - DoNotExtendIOException + AvoidCatchingThrowable MAJOR diff --git a/pom.xml b/pom.xml index ec96eb25..368c6277 100644 --- a/pom.xml +++ b/pom.xml @@ -108,11 +108,13 @@ 3.3.1 2.0.17 2.20.0 + 1.0.0 sonar-pmd-lib sonar-pmd-plugin + sonar-pmd-apex-plugin integration-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.

+

Example

+

 // Incorrect:
+ @istest
+ private static void fooShouldBar() {
+   //...
+ }
+ 
+ // Correct:
+ @IsTest
+ private static void fooShouldBar() {
+   //...
+ }
+ 
+ //Incorrect:
+ @testvisible
+ private boolean doSomething = false;
+ 
+ 
+ //Correct:
+ @TestVisible
+ private boolean doSomething = false;

+

Full documentation

]]>
+ 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
+     }
+ }

+

Full documentation

]]>
+ 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);
+ }

+

Full documentation

]]>
+ 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.
+

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

+

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

+

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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.

+

Example

+

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

+

Full documentation

]]>
+ 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();
+     }
+ }

+

Full documentation

]]>
+ 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; }
+ }

+

Full documentation

]]>
+ 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');
+     }
+ }

+

Full documentation

]]>
+ 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);
+     }
+ }

+

Full documentation

]]>
+ pmd + security +
+ + ApexSOQLInjection + Apex SOQLInjection + category/apex/security.xml/ApexSOQLInjection + MAJOR + Title of issues: Avoid untrusted/unescaped variables in DML query +

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

+

Example

+

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

+

Full documentation

]]>
+ 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
+ }

+

Full documentation

]]>
+ 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.

+

For more information, you can check this

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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();
+     }
+ }

+

Full documentation

]]>
+ 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());
+           }
+     }
+ }

+

Full documentation

]]>
+ 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.

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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();
+     }
+ }

+

Full documentation

]]>
+ 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);
+ }

+

Full documentation

]]>
+ pmd + security +
+ + ApexXSSFromURLParam + Apex XSSFrom URLParam + category/apex/security.xml/ApexXSSFromURLParam + MAJOR + Title of issues: Apex classes should escape/sanitize Strings obtained from URL parameters +

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + security +
+ + AvoidBooleanMethodParameters + Avoid boolean method parameters + category/apex/design.xml/AvoidBooleanMethodParameters + CRITICAL + Title of issues: Avoid Boolean method parameters +

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

+

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

+

Examples

+

Example 1

+

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

+

Example 2

+

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

+

Full documentation

]]>
+ pmd + 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
+         }
+     }
+ }

+

Full documentation

]]>
+ 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
+                 }
+             }
+         }
+     }
+ }

+

Full documentation

]]>
+ 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.
+    }
+ }

+

Full documentation

]]>
+ 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) {
+         // ...
+     }
+ }

+

Full documentation

]]>
+ 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...
+         }
+     }
+ }

+

Full documentation

]]>
+ 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).

+

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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() {
+         // ...
+     }
+ }

+

Full documentation

]]>
+ 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
+     }
+ }

+

Full documentation

]]>
+ 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;
+   }
+ }

+

Full documentation

]]>
+ 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

+

Full documentation

]]>
+ 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.

+

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

+

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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;
+       }
+     }
+   }
+ }

+

Full documentation

]]>
+ 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
+     }
+ }

+

Full documentation

]]>
+ 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;
+         }
+     }
+ }

+

Full documentation

]]>
+ pmd + performance + + noDefault + + false + BOOLEAN + +
+ + EmptyCatchBlock + Empty catch block + category/apex/errorprone.xml/EmptyCatchBlock + MAJOR + Title of issues: Avoid empty catch blocks +

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + errorprone + + allowCommentedBlocks + + false + BOOLEAN + + + allowExceptionNameRegex + + ^(ignored|expected)$ + STRING + +
+ + EmptyIfStmt + Empty if stmt + category/apex/errorprone.xml/EmptyIfStmt + MAJOR + Title of issues: Avoid empty 'if' statements +

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + errorprone +
+ + EmptyStatementBlock + Empty statement block + category/apex/errorprone.xml/EmptyStatementBlock + MAJOR + Title of issues: Avoid empty block statements. +

Empty block statements serve no purpose and should be removed.

+

Example

+

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

+

Full documentation

]]>
+ pmd + errorprone + + reportEmptyPrivateNoArgConstructor + + true + BOOLEAN + + + reportEmptyVirtualMethod + + true + BOOLEAN + +
+ + EmptyTryOrFinallyBlock + Empty try or finally block + category/apex/errorprone.xml/EmptyTryOrFinallyBlock + MAJOR + Title of issues: Avoid empty try or finally blocks +

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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!
+   }
+ }

+

Full documentation

]]>
+ 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
+     }
+ }

+

Full documentation

]]>
+ 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) {
+     // ...
+ }

+

Full documentation

]]>
+ 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 ...]
+ }

+

Full documentation

]]>
+ 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
+ }

+

Full documentation

]]>
+ 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
+ }

+

Full documentation

]]>
+ 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();
+ }

+

Full documentation

]]>
+ 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
+ }

+

Full documentation

]]>
+ 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;

+

Full documentation

]]>
+ 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++;
+ }

+

Full documentation

]]>
+ 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;
+     }
+ }

+

Full documentation

]]>
+ 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
+     }
+ }

+

Full documentation

]]>
+ 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

+

Full documentation

]]>
+ 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() {}
+ }

+

Full documentation

]]>
+ 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();
+ }
+ }

+

Full documentation

]]>
+ 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;
+     }
+ }

+

Full documentation

]]>
+ 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();
+     }
+ }

+

Full documentation

]]>
+ 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;

+

Full documentation

]]>
+ 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);
+             }
+         }
+     }
+ }

+

Full documentation

]]>
+ 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);
+         }
+     }
+ }

+

Full documentation

]]>
+ 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.

+

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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
+ }

+

Full documentation

]]>
+ 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
+         }
+     }
+ }

+

Full documentation

]]>
+ 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;
+         }
+     }
+ }

+

Full documentation

]]>
+ 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.

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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;
+ }

+

Full documentation

]]>
+ 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';
+     }
+ }

+

Full documentation

]]>
+ 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);
+     }

+

Full documentation

]]>
+ 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: +

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

+

Example

+

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

+

Full documentation

]]>
+ pmd + 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++;
+ }

+

Full documentation

]]>
+ 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 + jspecify compile + + org.jdom + jdom2 + ${jdom2.version} + net.sourceforge.pmd pmd-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; + } + List list = Arrays.asList(extensions); + context.addExtensions(list); + } + + /** + * Creates the hidden PropertyDefinition for the XML report switch used by PMD execution. + * The property key should be {@code org.sonar.plugins.pmd.PmdConfiguration#PROPERTY_GENERATE_XML}. + * + * @param propertyKey The configuration property key + * @return PropertyDefinition ready to be added to the Plugin context + */ + public static PropertyDefinition xmlReportProperty(String propertyKey) { + return PropertyDefinition.builder(propertyKey) + .defaultValue("false") + .name("Generate XML Report") + .hidden() + .build(); + } +} diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdProperty.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdProperty.java new file mode 100644 index 00000000..396b9ec6 --- /dev/null +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdProperty.java @@ -0,0 +1,40 @@ +/* + * Moved from apex/plugin modules to shared lib to reduce duplication. + */ +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/PmdRule.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRule.java similarity index 77% rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRule.java rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRule.java index b7158c42..2aff48cd 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRule.java +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRule.java @@ -1,27 +1,11 @@ /* - * 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. + * Moved to lib to share between plugins. */ package org.sonar.plugins.pmd.xml; +import org.jspecify.annotations.Nullable; import org.sonar.plugins.pmd.PmdConstants; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSet.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSet.java similarity index 99% rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSet.java rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSet.java index a18e1f28..5d39cb08 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSet.java +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSet.java @@ -24,8 +24,6 @@ import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; - import org.apache.commons.lang3.StringUtils; import org.jdom2.CDATA; import org.jdom2.Document; @@ -33,6 +31,7 @@ import org.jdom2.Namespace; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; +import org.jspecify.annotations.Nullable; public class PmdRuleSet { diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSets.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSets.java similarity index 70% rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSets.java rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSets.java index 87651cc8..836e92eb 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSets.java +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/PmdRuleSets.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 in lib to avoid duplication across modules. */ package org.sonar.plugins.pmd.xml; @@ -39,9 +23,6 @@ import java.util.Enumeration; import java.util.List; -/** - * Convenience class that creates {@link PmdRuleSet} instances out of the given input. - */ public class PmdRuleSets { private static final Logger LOG = LoggerFactory.getLogger(PmdRuleSets.class); @@ -51,26 +32,39 @@ public class PmdRuleSets { private static PmdRuleScopeRegistry createRegistry() { try { PmdRuleScopeRegistry registry = PmdRuleScopeRegistry.getInstance(); - registry.addXmlResources( - "/org/sonar/plugins/pmd/rules-java.xml", - "/org/sonar/plugins/pmd/rules-kotlin.xml" - ); ClassLoader cl = PmdRuleSets.class.getClassLoader(); - loadPluginProvidedSonarRules(cl, registry); + loadPmdJavaAndKotlineRules(cl, registry); + + loadChildPmdPluginProvidedSonarRules(cl, registry); loadJPinpointPluginRules(cl, registry); return registry; } catch (Exception e) { LOG.error("Failed to initialize PMD scope registry, using empty registry", e); - return PmdRuleScopeRegistry.getInstance(); // Empty or minimal fallback + return PmdRuleScopeRegistry.getInstance(); + } + } + + private static void loadPmdJavaAndKotlineRules(ClassLoader cl, PmdRuleScopeRegistry registry) { + // Only load Java/Kotlin rule-scope resources if they exist on the classpath + String javaPath = "org/sonar/plugins/pmd/rules-java.xml"; + String kotlinPath = "org/sonar/plugins/pmd/rules-kotlin.xml"; + if (cl.getResource(javaPath) != null) { + registry.addXmlResources("/" + javaPath); + } + if (cl.getResource(kotlinPath) != null) { + registry.addXmlResources("/" + kotlinPath); } } + /** + * Needed for backwards compatibility for the child PMD plugin with JPinpoint rules + * Can be removed when the plugin has its own META-INF/sonar-pmd/sonar-pmd-rules-paths.txt + */ private static void loadJPinpointPluginRules(ClassLoader cl, PmdRuleScopeRegistry registry) { - // Load jPinpoint Sonar plugin sonar rules if present on classpath (new canonical path only) try { String jpinpointPath = "com/jpinpoint/sonar/rules/sonar-pmd-jpinpoint.xml"; List found = new ArrayList<>(); @@ -87,9 +81,8 @@ private static void loadJPinpointPluginRules(ClassLoader cl, PmdRuleScopeRegistr } } - private static void loadPluginProvidedSonarRules(ClassLoader cl, PmdRuleScopeRegistry registry) { - // Support a generic index file that child plugins can ship to declare arbitrary rule XML paths - String indexResource = "META-INF/sonar-pmd/sonar-pmd-rules-paths.txt"; // each line: a classpath resource path to an XML + private static void loadChildPmdPluginProvidedSonarRules(ClassLoader cl, PmdRuleScopeRegistry registry) { + String indexResource = "META-INF/sonar-pmd/sonar-pmd-rules-paths.txt"; try { Enumeration indexUrls = cl.getResources(indexResource); int processedIndexes = 0; @@ -122,7 +115,6 @@ private static void processSonarPluginRulesPathLine(ClassLoader cl, String line, if (path.isEmpty() || path.startsWith("#")) { return; } - // Normalize leading slash for ClassLoader lookups if (path.startsWith("/")) { path = path.substring(1); } @@ -138,20 +130,10 @@ private static void processSonarPluginRulesPathLine(ClassLoader cl, String line, private PmdRuleSets() {} - /** - * @param configReader A character stream containing the data of the {@link PmdRuleSet}. - * @param messages SonarQube validation messages - allow to inform the enduser about processing problems. - * @return An instance of PmdRuleSet. The output may be empty but never null. - */ public static PmdRuleSet from(Reader configReader, ValidationMessages messages) { return createQuietly(new XmlRuleSetFactory(configReader, messages)); } - /** - * @param activeRules The currently active rules. - * @param repositoryKey The key identifier of the rule repository. - * @return An instance of PmdRuleSet. The output may be empty but never null. - */ public static PmdRuleSet from(ActiveRules activeRules, String repositoryKey) { return from(activeRules, repositoryKey, RuleScope.ALL); } diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/ActiveRulesRuleSetFactory.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/ActiveRulesRuleSetFactory.java similarity index 67% rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/ActiveRulesRuleSetFactory.java rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/ActiveRulesRuleSetFactory.java index ff9f6810..7e143ce1 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/ActiveRulesRuleSetFactory.java +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/ActiveRulesRuleSetFactory.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. + * Moved to lib to share between plugins. */ package org.sonar.plugins.pmd.xml.factory; @@ -30,9 +14,6 @@ import org.sonar.plugins.pmd.xml.PmdRule; import org.sonar.plugins.pmd.xml.PmdRuleSet; -/** - * Factory class to create {@link org.sonar.plugins.pmd.xml.PmdRuleSet} out of {@link org.sonar.api.batch.rule.ActiveRules}. - */ public class ActiveRulesRuleSetFactory implements RuleSetFactory { private final ActiveRules activeRules; @@ -73,12 +54,8 @@ private boolean isRuleInScope(ActiveRule activeRule, RuleScope targetScope) { return true; } - // Get the actual scope from the registry (based on XML rule definition) RuleScope ruleScope = scopeRegistry.getScope(activeRule.ruleKey().rule()); - // Rule matches if: - // - Rule scope is ALL (applies to both MAIN and TEST) - // - Rule scope matches the target scope exactly return ruleScope == RuleScope.ALL || ruleScope == targetScope; } @@ -94,6 +71,6 @@ private void addRuleProperties(ActiveRule activeRule, PmdRule pmdRule) { @Override public void close() { - // Unnecessary in this class. + // no-op } } diff --git a/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java new file mode 100644 index 00000000..8f35cf6c --- /dev/null +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/RuleSetFactory.java @@ -0,0 +1,13 @@ +/* + * Shared RuleSetFactory interface in lib. + */ +package org.sonar.plugins.pmd.xml.factory; + +import java.io.Closeable; + +import org.sonar.plugins.pmd.xml.PmdRuleSet; + +public interface RuleSetFactory extends Closeable { + + PmdRuleSet create(); +} diff --git a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/XmlRuleSetFactory.java b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/XmlRuleSetFactory.java similarity index 75% rename from sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/XmlRuleSetFactory.java rename to sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/XmlRuleSetFactory.java index d08a07ad..d96c32b7 100644 --- a/sonar-pmd-plugin/src/main/java/org/sonar/plugins/pmd/xml/factory/XmlRuleSetFactory.java +++ b/sonar-pmd-lib/src/main/java/org/sonar/plugins/pmd/xml/factory/XmlRuleSetFactory.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. + * Moved to lib to share between plugins. */ package org.sonar.plugins.pmd.xml.factory; @@ -24,6 +8,7 @@ import org.jdom2.JDOMException; import org.jdom2.Namespace; import org.jdom2.input.SAXBuilder; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.utils.ValidationMessages; @@ -31,15 +16,11 @@ import org.sonar.plugins.pmd.xml.PmdRule; import org.sonar.plugins.pmd.xml.PmdRuleSet; -import javax.annotation.Nullable; import javax.xml.XMLConstants; import java.io.IOException; import java.io.Reader; import java.util.List; -/** - * Factory class to create {@link org.sonar.plugins.pmd.xml.PmdRuleSet} out of XML. - */ public class XmlRuleSetFactory implements RuleSetFactory { private static final Logger LOG = LoggerFactory.getLogger(XmlRuleSetFactory.class); @@ -53,7 +34,6 @@ public XmlRuleSetFactory(Reader source, ValidationMessages messages) { this.messages = messages; } - private List getChildren(Element parent, String childName, @Nullable Namespace namespace) { if (namespace == null) { return parent.getChildren(childName); @@ -82,25 +62,14 @@ private void parsePmdPriority(Element eltRule, PmdRule pmdRule, @Nullable Namesp } } - /** - * Closes all resources. - * - * @throws IOException If an I/O error occurs. - */ @Override public void close() throws IOException { source.close(); } - /** - * Parses the given Reader for PmdRuleSets. - * - * @return The extracted PmdRuleSet - empty in case of problems, never null. - */ @Override public PmdRuleSet create() { final SAXBuilder builder = new SAXBuilder(); - // prevent XXE attacks builder.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); builder.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); final Document dom; diff --git a/sonar-pmd-plugin/pom.xml b/sonar-pmd-plugin/pom.xml index 6502cd82..d8ece78b 100644 --- a/sonar-pmd-plugin/pom.xml +++ b/sonar-pmd-plugin/pom.xml @@ -62,6 +62,7 @@ org.sonarsource.java java-frontend ${sonar-java.version} + compile org.sonarsource.pmd @@ -173,14 +174,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - - - -Xmx512m - - org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin @@ -205,7 +198,7 @@ - 30000000 + 60000000 12000000 ${project.build.directory}/${project.build.finalName}.jar @@ -230,7 +223,6 @@ generate-pmd-rules - org.codehaus.gmaven groovy-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();