Repository: SpoonLabs/sorald (java)

Issue:
S1176: tailor S1176 (Javadoc) 
easy solution: we remove the `api` FQN constraint for S1176.

Pull request:
feat: add parameters for individual rules while mining
Fixes #807 

Some SonarSource rules have parameters which determine if the rule needs to be reported or not. See [`forClasses`](https://github.com/SonarSource/sonar-java/blob/cbf1e883e99c0c54b499b8663543fc6449aa856e/java-checks/src/main/java/org/sonar/java/checks/UndocumentedApiCheck.java#L59) for example. The changes add the ability to pass such options to a static analyzer via the CLI option - `--rule-parameters`. The option takes a valid JSON file with the following format:
```json
{
    "<RULE_KEY>": {
        "<RULE_PROPERTY_NAME>": "<VALUE>"
    }
}
```

This PR, if ready for merge, must be merged after #815 . 

Patch:
diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml
index ba1e710..4efeea1 100644
--- a/.github/workflows/support.yml
+++ b/.github/workflows/support.yml
@@ -58,9 +58,13 @@ jobs:
           java-package: jdk
           architecture: x64
           distribution: ${{ env.JAVA_DISTRIBUTION }}
-      - name: Genrate usage for the subcommands and submit a PR if modified
+      - name: Generate usage for the subcommands and submit a PR if modified
         run: |
-          mvn process-classes
+          mvn clean compile dependency:build-classpath -Dmdep.outputFile=cp.txt
+          (cd sorald && java -cp $(cat cp.txt):target/classes/ picocli.codegen.docgen.manpage.ManPageGenerator \
+            --outdir=../docs/usage/ \
+            sorald.cli.MineCommand \
+            sorald.cli.RepairCommand)
           
           ./.github/submit-pr.sh \
             --branch-prefix   usage \
diff --git a/sorald-api/src/main/java/sorald/Constants.java b/sorald-api/src/main/java/sorald/Constants.java
index b8003d7..fd5c738 100644
--- a/sorald-api/src/main/java/sorald/Constants.java
+++ b/sorald-api/src/main/java/sorald/Constants.java
@@ -24,6 +24,7 @@ public class Constants {
     public static final String ARG_RULE_VIOLATION_SPECIFIERS = "--violation-specs";
     public static final String ARG_TARGET = "--target";
     public static final String ARG_RESOLVE_CLASSPATH_FROM = "--resolve-classpath-from";
+    public static final String ARG_RULE_PARAMETERS = "--rule-parameters";
 
     public static final String VIOLATION_SPECIFIER_SEP = File.pathSeparator;
 
diff --git a/sorald/pom.xml b/sorald/pom.xml
index b577922..11c9bf4 100644
--- a/sorald/pom.xml
+++ b/sorald/pom.xml
@@ -88,28 +88,6 @@
     </properties>
     <build>
         <plugins>
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>exec-maven-plugin</artifactId>
-                <version>3.0.0</version>
-                <executions>
-                    <execution>
-                        <id>generateManPages</id>
-                        <phase>process-classes</phase>
-                        <goals>
-                            <goal>java</goal>
-                        </goals>
-                    </execution>
-                </executions>
-                <configuration>
-                    <mainClass>picocli.codegen.docgen.manpage.ManPageGenerator</mainClass>
-                    <arguments>
-                        <argument>--outdir=${project.parent.basedir}/docs/usage</argument>
-                        <argument>sorald.cli.RepairCommand</argument>
-                        <argument>sorald.cli.MineCommand</argument>
-                    </arguments>
-                </configuration>
-            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-deploy-plugin</artifactId>
diff --git a/sorald/src/main/java/sorald/SoraldConfig.java b/sorald/src/main/java/sorald/SoraldConfig.java
index ce0b2a7..e3de2b5 100644
--- a/sorald/src/main/java/sorald/SoraldConfig.java
+++ b/sorald/src/main/java/sorald/SoraldConfig.java
@@ -3,8 +3,10 @@ package sorald;
 import java.io.File;
 import java.nio.file.Paths;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import sorald.cli.CLIConfigForStaticAnalyzer;
+import sorald.rule.Rule;
 
 /* All config settings of Sorald should be gathered here */
 public class SoraldConfig implements CLIConfigForStaticAnalyzer {
@@ -15,6 +17,7 @@ public class SoraldConfig implements CLIConfigForStaticAnalyzer {
     private int maxFilesPerSegment;
     private File statsOutputFile;
     private List<String> classpath;
+    private Map<Rule, Map<String, String>> ruleParameters;
 
     public SoraldConfig() {}
 
@@ -74,4 +77,14 @@ public class SoraldConfig implements CLIConfigForStaticAnalyzer {
     public List<String> getClasspath() {
         return classpath;
     }
+
+    public Map<Rule, Map<String, String>> getRuleParameters() {
+        return ruleParameters;
+    }
+
+    public CLIConfigForStaticAnalyzer setRuleParameters(
+            Map<Rule, Map<String, String>> ruleParameters) {
+        this.ruleParameters = ruleParameters;
+        return this;
+    }
 }
diff --git a/sorald/src/main/java/sorald/cli/MineCommand.java b/sorald/src/main/java/sorald/cli/MineCommand.java
index bc92fb6..a53023a 100644
--- a/sorald/src/main/java/sorald/cli/MineCommand.java
+++ b/sorald/src/main/java/sorald/cli/MineCommand.java
@@ -1,9 +1,11 @@
 package sorald.cli;
 
 import java.io.File;
+import java.io.IOException;
 import java.nio.file.Files;
 import java.util.*;
 import java.util.stream.Collectors;
+import org.json.JSONObject;
 import picocli.CommandLine;
 import sorald.Constants;
 import sorald.FileUtils;
@@ -15,6 +17,7 @@ import sorald.miner.MineSonarWarnings;
 import sorald.rule.IRuleType;
 import sorald.rule.Rule;
 import sorald.rule.RuleProvider;
+import sorald.sonar.SonarRule;
 import sorald.sonar.SonarRuleType;
 import sorald.util.MavenUtils;
 
@@ -65,6 +68,18 @@ class MineCommand extends BaseCommand {
                     "When this argument is used, Sorald only mines violations of the rules that can be fixed by Sorald.")
     private boolean handledRules;
 
+    @CommandLine.Option(
+            names = {Constants.ARG_RULE_PARAMETERS},
+            description = {
+                "Configuration for SonarJava rules.",
+                "Format of JSON file: {%n"
+                        + "    \"<RULE_KEY>\": {%n"
+                        + "        \"<RULE_PROPERTY_NAME>\": \"<VALUE>\"%n"
+                        + "    }%n"
+                        + "}"
+            })
+    private File ruleParameters;
+
     @Override
     public Integer call() throws Exception {
         validateArgs();
@@ -112,6 +127,10 @@ class MineCommand extends BaseCommand {
                             "%s is only supported for Maven projects, but %s has no pom.xml",
                             Constants.ARG_RESOLVE_CLASSPATH_FROM, source));
         }
+        if (ruleParameters != null && !ruleParameters.exists()) {
+            throw new CommandLine.ParameterException(
+                    spec.commandLine(), String.format("%s is not a valid file", ruleParameters));
+        }
     }
 
     private static class IRuleTypeConverter implements CommandLine.ITypeConverter<IRuleType> {
@@ -132,15 +151,33 @@ class MineCommand extends BaseCommand {
         }
     }
 
+    private CLIConfigForStaticAnalyzer createConfig() throws IOException {
+        SoraldConfig config = new SoraldConfig();
+        config.setClasspath(resolveClasspath());
+        config.setRuleParameters(parseRuleParameters());
+        return config;
+    }
+
     private List<String> resolveClasspath() {
         return resolveClasspathFrom != null
                 ? MavenUtils.resolveClasspath(resolveClasspathFrom.toPath())
                 : List.of();
     }
 
-    private CLIConfigForStaticAnalyzer createConfig() {
-        SoraldConfig config = new SoraldConfig();
-        config.setClasspath(resolveClasspath());
-        return config;
+    private Map<Rule, Map<String, String>> parseRuleParameters() throws IOException {
+        if (ruleParameters == null) {
+            return new HashMap<>();
+        }
+        JSONObject ruleParametersAsJson = FileUtils.readJSON(ruleParameters.toPath());
+        Map<Rule, Map<String, String>> result = new HashMap<>();
+        for (String rule : ruleParametersAsJson.keySet()) {
+            JSONObject parameters = (JSONObject) ruleParametersAsJson.get(rule);
+            Map<String, String> options = new HashMap<>();
+            for (String option : parameters.keySet()) {
+                options.put(option, (String) parameters.get(option));
+            }
+            result.put(new SonarRule(rule), options);
+        }
+        return result;
     }
 }
diff --git a/sorald/src/main/java/sorald/cli/RepairCommand.java b/sorald/src/main/java/sorald/cli/RepairCommand.java
index 971b9b9..a746203 100644
--- a/sorald/src/main/java/sorald/cli/RepairCommand.java
+++ b/sorald/src/main/java/sorald/cli/RepairCommand.java
@@ -3,11 +3,7 @@ package sorald.cli;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 import picocli.CommandLine;
 import sorald.*;
@@ -305,6 +301,7 @@ class RepairCommand extends BaseCommand {
         config.setRepairStrategy(repairStrategy);
         config.setStatsOutputFile(statsOutputFile);
         config.setClasspath(resolveClasspath());
+        config.setRuleParameters(new HashMap<>());
         return config;
     }
 }
diff --git a/sorald/src/main/java/sorald/sonar/SonarLintEngine.java b/sorald/src/main/java/sorald/sonar/SonarLintEngine.java
index 69effbb..3deec14 100644
--- a/sorald/src/main/java/sorald/sonar/SonarLintEngine.java
+++ b/sorald/src/main/java/sorald/sonar/SonarLintEngine.java
@@ -220,7 +220,16 @@ public final class SonarLintEngine extends AbstractSonarLintEngine {
 
         return allRulesDefinitionsByKey.values().stream()
                 .filter(isImplementedBySonarJavaPlugin(includedRules))
-                .map(rd -> new ActiveRule(rd.getKey(), rd.getLanguage().getLanguageKey()))
+                .map(
+                        rd -> {
+                            ActiveRule activeRule =
+                                    new ActiveRule(rd.getKey(), rd.getLanguage().getLanguageKey());
+                            RuleKey ruleKey = RuleKey.parse(rd.getKey());
+                            if (configuration.ruleParameters().containsKey(ruleKey)) {
+                                activeRule.setParams(configuration.ruleParameters().get(ruleKey));
+                            }
+                            return activeRule;
+                        })
                 .collect(Collectors.toList());
     }
 
diff --git a/sorald/src/main/java/sorald/sonar/SonarStaticAnalyzer.java b/sorald/src/main/java/sorald/sonar/SonarStaticAnalyzer.java
index cabad19..6c74509 100644
--- a/sorald/src/main/java/sorald/sonar/SonarStaticAnalyzer.java
+++ b/sorald/src/main/java/sorald/sonar/SonarStaticAnalyzer.java
@@ -2,15 +2,14 @@ package sorald.sonar;
 
 import com.google.auto.service.AutoService;
 import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 import org.sonarsource.sonarlint.core.client.api.common.RuleKey;
 import org.sonarsource.sonarlint.core.client.api.common.analysis.Issue;
 import org.sonarsource.sonarlint.core.client.api.common.analysis.IssueListener;
 import org.sonarsource.sonarlint.core.client.api.standalone.StandaloneAnalysisConfiguration;
+import sorald.SoraldConfig;
 import sorald.cli.CLIConfigForStaticAnalyzer;
 import sorald.rule.Rule;
 import sorald.rule.RuleViolation;
@@ -75,6 +74,8 @@ public class SonarStaticAnalyzer implements StaticAnalyzer {
                         rules.stream()
                                 .map(rule -> RuleKey.parse(String.format("java:%s", rule.getKey())))
                                 .collect(Collectors.toList()))
+                .addRuleParameters(
+                        putRuleParameters(((SoraldConfig) cliOptions).getRuleParameters()))
                 .addInputFiles(inputFiles)
                 .build();
     }
@@ -91,6 +92,20 @@ public class SonarStaticAnalyzer implements StaticAnalyzer {
                 .build();
     }
 
+    private static Map<RuleKey, Map<String, String>> putRuleParameters(
+            Map<Rule, Map<String, String>> passedRuleParameters) {
+        Map<RuleKey, Map<String, String>> parsedRuleParameters = new HashMap<>();
+        passedRuleParameters
+                .keySet()
+                .forEach(
+                        rule -> {
+                            parsedRuleParameters.put(
+                                    RuleKey.parse(String.format("java:%s", rule.getKey())),
+                                    passedRuleParameters.get(rule));
+                        });
+        return parsedRuleParameters;
+    }
+
     private static class IssueHandler implements IssueListener {
         private final List<Issue> issues = new ArrayList<>();
 


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 91b21f9..f8d3c65 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -141,7 +141,7 @@ jobs:
       - name: Test that generated code is up-to-date
         run: |
           mvn install -DskipTests
-          cd sorald && mvn clean compile dependency:build-classpath -Dmdep.outputFile=cp.txt
-          cd sorald && java -cp "$(cat cp.txt):.target/classes" sorald.CodeGenerator
-          cd sorald && mvn spotless:apply
+          mvn clean compile dependency:build-classpath -Dmdep.outputFile=cp.txt
+          (cd sorald && java -cp "$(cat cp.txt):target/classes" sorald.CodeGenerator)
+          mvn spotless:apply
           git diff --exit-code
diff --git a/sorald/src/test/java/sorald/miner/WarningMinerTest.java b/sorald/src/test/java/sorald/miner/WarningMinerTest.java
index 6f7fdd7..92c190e 100644
--- a/sorald/src/test/java/sorald/miner/WarningMinerTest.java
+++ b/sorald/src/test/java/sorald/miner/WarningMinerTest.java
@@ -268,6 +268,60 @@ public class WarningMinerTest {
         }
     }
 
+    @Nested
+    class MineWarningsWithRuleParameters {
+        @Test
+        void S1176_shouldNotBeReportedIfParametersAreNotPassed() {
+            String[] args = {
+                Constants.MINE_COMMAND_NAME,
+                Constants.ARG_SOURCE,
+                TestHelper.PATH_TO_RESOURCES_FOLDER
+                        .resolve("warning_miner")
+                        .resolve("rule_parameters")
+                        .resolve("S1176")
+                        .resolve("Javadoc.java")
+                        .toString(),
+            };
+
+            // act
+            final ByteArrayOutputStream out = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(out));
+            Main.main(args);
+
+            // assert
+            assertThat(out.toString(), not(containsString("S1176=1")));
+        }
+
+        @Test
+        void S1176_shouldBeReported() {
+            String[] args = {
+                Constants.MINE_COMMAND_NAME,
+                Constants.ARG_SOURCE,
+                TestHelper.PATH_TO_RESOURCES_FOLDER
+                        .resolve("warning_miner")
+                        .resolve("rule_parameters")
+                        .resolve("S1176")
+                        .resolve("Javadoc.java")
+                        .toString(),
+                Constants.ARG_RULE_PARAMETERS,
+                TestHelper.PATH_TO_RESOURCES_FOLDER
+                        .resolve("warning_miner")
+                        .resolve("rule_parameters")
+                        .resolve("S1176")
+                        .resolve("params.json")
+                        .toString(),
+            };
+
+            // act
+            final ByteArrayOutputStream out = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(out));
+            Main.main(args);
+
+            // assert
+            assertThat(out.toString(), containsString("S1176=1"));
+        }
+    }
+
     private static void runMiner(
             Path pathToRepos, String pathToOutput, String pathToTempDir, String... extraArgs)
             throws Exception {
diff --git a/sorald/src/test/resources/warning_miner/rule_parameters/S1176/Javadoc.java b/sorald/src/test/resources/warning_miner/rule_parameters/S1176/Javadoc.java
new file mode 100644
index 0000000..7f6c186
--- /dev/null
+++ b/sorald/src/test/resources/warning_miner/rule_parameters/S1176/Javadoc.java
@@ -0,0 +1,1 @@
+public class Javadoc { }
diff --git a/sorald/src/test/resources/warning_miner/rule_parameters/S1176/params.json b/sorald/src/test/resources/warning_miner/rule_parameters/S1176/params.json
new file mode 100644
index 0000000..703182b
--- /dev/null
+++ b/sorald/src/test/resources/warning_miner/rule_parameters/S1176/params.json
@@ -0,0 +1,5 @@
+{
+  "S1176": {
+    "forClasses": "**"
+  }
+}