Repository: analysis-dev/diktat (kotlin)

Issue:
Get on track to GitHub actions and SARIF 
We should do the following:
- support reporterType in maven plugin 
- add proper support of SARIF format to gradle plugin

Everything is already done in ktlint (https://github.com/pinterest/ktlint/issues/1102)

Pull request:
### What's done:
- Fixing gradle plugin reporter to support Sarif
- Adding the support of reporters to maven plugin 

This pull request partially closes https://github.com/analysis-dev/diktat/issues/1179

Patch:
diff --git a/README.md b/README.md
index 707fbb8226..1c7d41036a 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,8 @@ Main features of diktat are the following:
 4) **Strict detailed coding convention** that you can use in your project.
 
 ## Run as CLI-application
+<details>
+<summary>Download and install binaries:</summary>
 1. Install KTlint manually: [here](https://github.com/pinterest/ktlint/releases)
 
    **OR** use curl:
@@ -68,12 +70,34 @@ Main features of diktat are the following:
    ```
 
 To **autofix** all code style violations use `-F` option.
+</details>
+
+## GitHub Native Integration
+We suggest everyone to use common ["sarif"](https://docs.oasis-open.org/sarif/sarif/v2.0/sarif-v2.0.html) format as a `reporterType` in CI/CD.
+GitHub has an [integration](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) 
+with SARIF format and provides you a native reporting of diktat issues in Pull Requests. 
+
+```text
+    reporterType = "sarif"
+    output = "diktat-report.sarif"
+```
+
+Add the following code to your GitHub Action to upload diktat sarif report (after it was generated).
+
+```yml
+      - name: Upload SARIF to Github using the upload-sarif action
+        uses: github/codeql-action/upload-sarif@v1
+        if: ${{ always() }}
+        with:
+          sarif_file: build/diktat-report.sarif
+```
 
 ## Run with Maven using diktat-maven-plugin
 This plugin is available since version 0.1.3. You can see how it is configured in our project for self-checks: [pom.xml](pom.xml).
 If you use it and encounter any problems, feel free to open issues on [github](https://github.com/cqfn/diktat/issues).
 
-Add this plugin to your pom.xml:
+<details>
+<summary>Add this plugin to your pom.xml:</summary>
 ```xml
             <plugin>
                 <groupId>org.cqfn.diktat</groupId>
@@ -101,6 +125,7 @@ Add this plugin to your pom.xml:
                 </executions>
             </plugin>
 ```
+</details>
 
 To run diktat in **only-check** mode use command `$ mvn diktat:check@diktat`.
 To run diktat in **autocorrect** mode use command `$ mvn diktat:fix@diktat`.
@@ -109,7 +134,9 @@ To run diktat in **autocorrect** mode use command `$ mvn diktat:fix@diktat`.
 Requires a gradle version no lower than 5.3.
 
 This plugin is available since version 0.1.5. You can see how the plugin is configured in our examples: [build.gradle.kts](examples/gradle-kotlin-dsl/build.gradle.kts).
-Add this plugin to your `build.gradle.kts`:
+
+<details>
+<summary>Add this plugin to your `build.gradle.kts`:</summary>
 ```kotlin
 plugins {
     id("org.cqfn.diktat.diktat-gradle-plugin") version "1.0.2"
@@ -141,29 +168,21 @@ diktat {
 }
 ```
 
-Also `diktat` extension has different reporters. You can specify `json`, `html`, `checkstyle`, `plain` (default) or your own custom reporter:
+Also `diktat` extension has different reporters. You can specify `json`, `html`, `sarif`, `plain` (default) or your own custom reporter (it should be added as a dependency into `diktat` configuration):
 ```kotlin
 diktat {
-    reporter = "json" // "html", "checkstyle", "plain"
+   reporterType = "json" // "html", "json", "plain" (default), "sarif"
 }
 ```
 
-Example of your custom reporter:
-```kotlin
-diktat {
-    reporter = "custom:name:pathToJar"
-}
-```
-Name parameter is the name of your reporter and as the last parameter you should specify path to jar, which contains your reporter.
-[Example of the junit custom reporter.](https://github.com/kryanod/ktlint-junit-reporter)
-
 You can also specify an output. 
 ```kotlin
 diktat {
-    reporter = "json"
+    reporterType = "json"
     output = "someFile.json"
 }
 ```
+</details>
 
 You can run diktat checks using task `diktatCheck` and automatically fix errors with tasks `diktatFix`.
 
diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatExtension.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatExtension.kt
index abd56fed5e..18ff49ea3f 100644
--- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatExtension.kt
+++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatExtension.kt
@@ -27,7 +27,7 @@ open class DiktatExtension(
     /**
      * Type of the reporter to use
      */
-    var reporterType: String = "plain"
+    var reporter: String = "plain"
 
     /**
      * Type of output
diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt
index ca9442ff6a..3b11de7bc4 100644
--- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt
+++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskBase.kt
@@ -87,10 +87,6 @@ open class DiktatJavaExecTaskBase @Inject constructor(
             main = "com.pinterest.ktlint.Main"
         }
 
-        // Plain, checkstyle and json reporter are provided out of the box in ktlint
-        if (diktatExtension.reporterType == "html") {
-            diktatConfiguration.dependencies.add(project.dependencies.create("com.pinterest.ktlint:ktlint-reporter-html:$KTLINT_VERSION"))
-        }
         classpath = diktatConfiguration
         project.logger.debug("Setting diktatCheck classpath to ${diktatConfiguration.dependencies.toSet()}")
         if (diktatExtension.debug) {
@@ -155,13 +151,8 @@ open class DiktatJavaExecTaskBase @Inject constructor(
     private fun createReporterFlag(diktatExtension: DiktatExtension): String {
         val flag: StringBuilder = StringBuilder()
 
-        // Plain, checkstyle and json reporter are provided out of the box in ktlint
-        when (diktatExtension.reporterType) {
-            "json" -> flag.append("--reporter=json")
-            "html" -> flag.append("--reporter=html")
-            "checkstyle" -> flag.append("--reporter=checkstyle")
-            else -> customReporter(diktatExtension, flag)
-        }
+        // appending the flag with the reporter
+        setReporter(diktatExtension, flag)
 
         if (diktatExtension.output.isNotEmpty()) {
             flag.append(",output=${diktatExtension.output}")
@@ -170,19 +161,14 @@ open class DiktatJavaExecTaskBase @Inject constructor(
         return flag.toString()
     }
 
-    private fun customReporter(diktatExtension: DiktatExtension, flag: java.lang.StringBuilder) {
-        if (diktatExtension.reporterType.startsWith("custom")) {
-            val name = diktatExtension.reporterType.split(":")[1]
-            val jarPath = diktatExtension.reporterType.split(":")[2]
-            if (name.isEmpty() || jarPath.isEmpty()) {
-                project.logger.warn("Either name or path to jar is not specified. Falling to plain reporter")
-                flag.append("--reporter=plain")
-            } else {
-                flag.append("--reporter=$name,artifact=$jarPath")
-            }
-        } else {
+    private fun setReporter(diktatExtension: DiktatExtension, flag: java.lang.StringBuilder) {
+        val name = diktatExtension.reporter.trim()
+        val validReporters = listOf("sarif", "plain", "json", "html")
+        if (name.isEmpty() || !validReporters.contains(name)) {
+            project.logger.warn("Reporter name $name was not specified or is invalid. Falling to 'plain' reporter")
             flag.append("--reporter=plain")
-            project.logger.debug("Unknown reporter was specified. Falling back to plain reporter.")
+        } else {
+            flag.append("--reporter=$name")
         }
     }
 
diff --git a/diktat-maven-plugin/pom.xml b/diktat-maven-plugin/pom.xml
index 5fab75ffaf..b8dba2f2f9 100644
--- a/diktat-maven-plugin/pom.xml
+++ b/diktat-maven-plugin/pom.xml
@@ -54,6 +54,21 @@
             <artifactId>ktlint-reporter-plain</artifactId>
             <version>${ktlint.version}</version>
         </dependency>
+        <dependency>
+            <groupId>com.pinterest.ktlint</groupId>
+            <artifactId>ktlint-reporter-sarif</artifactId>
+            <version>${ktlint.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.pinterest.ktlint</groupId>
+            <artifactId>ktlint-reporter-json</artifactId>
+            <version>${ktlint.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.pinterest.ktlint</groupId>
+            <artifactId>ktlint-reporter-html</artifactId>
+            <version>${ktlint.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.junit.vintage</groupId>
             <artifactId>junit-vintage-engine</artifactId>
diff --git a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt
index 88b87fe277..d0408ecd4e 100644
--- a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt
+++ b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt
@@ -4,9 +4,13 @@ import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider
 
 import com.pinterest.ktlint.core.KtLint
 import com.pinterest.ktlint.core.LintError
+import com.pinterest.ktlint.core.Reporter
 import com.pinterest.ktlint.core.RuleExecutionException
 import com.pinterest.ktlint.core.RuleSet
+import com.pinterest.ktlint.reporter.html.HtmlReporter
+import com.pinterest.ktlint.reporter.json.JsonReporter
 import com.pinterest.ktlint.reporter.plain.PlainReporter
+import com.pinterest.ktlint.reporter.sarif.SarifReporter
 import org.apache.maven.plugin.AbstractMojo
 import org.apache.maven.plugin.MojoExecutionException
 import org.apache.maven.plugin.MojoFailureException
@@ -14,6 +18,8 @@ import org.apache.maven.plugins.annotations.Parameter
 import org.apache.maven.project.MavenProject
 
 import java.io.File
+import java.io.FileOutputStream
+import java.io.PrintStream
 
 /**
  * Base [Mojo] for checking and fixing code using diktat
@@ -25,8 +31,19 @@ abstract class DiktatBaseMojo : AbstractMojo() {
     @Parameter(property = "diktat.debug")
     var debug = false
 
-    // FixMe: Reporter should be chosen via plugin configuration
-    private val reporter = PlainReporter(System.out)
+    /**
+     * Type of the reporter to use
+     */
+    @Parameter(property = "diktat.reporter")
+    var reporter = "plain"
+
+    /**
+     * Type of output
+     * Default: System.out
+     */
+    @Parameter(property = "diktat.output")
+    var output = ""
+    private lateinit var reporterImpl: Reporter
 
     /**
      * Path to diktat yml config file. Can be either absolute or relative to project's root directory.
@@ -64,6 +81,7 @@ abstract class DiktatBaseMojo : AbstractMojo() {
      * @throws MojoExecutionException if [RuleExecutionException] has been thrown
      */
     override fun execute() {
+        reporterImpl = resolveReporter()
         val configFile = resolveConfig()
         if (!File(configFile).exists()) {
             throw MojoExecutionException("Configuration file $diktatConfigFile doesn't exist")
@@ -83,12 +101,26 @@ abstract class DiktatBaseMojo : AbstractMojo() {
                 checkDirectory(it, lintErrors, ruleSets)
             }
 
-        reporter.afterAll()
+        reporterImpl.afterAll()
         if (lintErrors.isNotEmpty()) {
             throw MojoFailureException("There are ${lintErrors.size} lint errors")
         }
     }
 
+    private fun resolveReporter(): Reporter {
+        val output = if (this.output.isBlank()) System.`out` else PrintStream(FileOutputStream(this.output, true))
+        return when (this.reporter) {
+            "sarif" -> SarifReporter(output)
+            "plain" -> PlainReporter(output)
+            "json" -> JsonReporter(output)
+            "html" -> HtmlReporter(output)
+            else -> {
+                log.warn("Reporter name ${this.reporter} was not specified or is invalid. Falling to 'plain' reporter")
+                PlainReporter(output)
+            }
+        }
+    }
+
     /**
      * Function that searches diktat config file in maven project hierarchy.
      * If [diktatConfigFile] is absolute, it's path is used. If [diktatConfigFile] is relative, this method looks for it in all maven parent projects.
@@ -128,9 +160,9 @@ abstract class DiktatBaseMojo : AbstractMojo() {
             .forEach { file ->
                 log.debug("Checking file $file")
                 try {
-                    reporter.before(file.path)
+                    reporterImpl.before(file.path)
                     checkFile(file, lintErrors, ruleSets)
-                    reporter.after(file.path)
+                    reporterImpl.after(file.path)
                 } catch (e: RuleExecutionException) {
                     log.error("Unhandled exception during rule execution: ", e)
                     throw MojoExecutionException("Unhandled exception during rule execution", e)
@@ -151,7 +183,7 @@ abstract class DiktatBaseMojo : AbstractMojo() {
                     userData = mapOf("file_path" to file.path),
                     script = file.extension.equals("kts", ignoreCase = true),
                     cb = { lintError, isCorrected ->
-                        reporter.onLintError(file.path, lintError, isCorrected)
+                        reporterImpl.onLintError(file.path, lintError, isCorrected)
                         lintErrors.add(lintError)
                     },
                     debug = debug


Test patch:
diff --git a/diktat-gradle-plugin/src/functionalTest/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginFunctionalTest.kt b/diktat-gradle-plugin/src/functionalTest/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginFunctionalTest.kt
index 723368d3eb..1d6f19d9f5 100644
--- a/diktat-gradle-plugin/src/functionalTest/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginFunctionalTest.kt
+++ b/diktat-gradle-plugin/src/functionalTest/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginFunctionalTest.kt
@@ -45,7 +45,7 @@ class DiktatGradlePluginFunctionalTest {
             """${System.lineSeparator()}
                 diktat {
                     inputs { include("src/**/*.kt") }
-                    reporterType = "json"
+                    reporter = "json"
                     output = "test.txt"
                 }
             """.trimIndent()
diff --git a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt
index 439b8ee823..b332471742 100644
--- a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt
+++ b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePluginTest.kt
@@ -37,6 +37,6 @@ class DiktatGradlePluginTest {
     @Test
     fun `check that the right reporter dependency added`() {
         val diktatExtension = project.extensions.getByName("diktat") as DiktatExtension
-        Assertions.assertTrue(diktatExtension.reporterType == "plain")
+        Assertions.assertTrue(diktatExtension.reporter == "plain")
     }
 }
diff --git a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt
index 0d3710723d..146c4d68e7 100644
--- a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt
+++ b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt
@@ -94,7 +94,7 @@ class DiktatJavaExecTaskTest {
         ) {
             inputs { exclude("*") }
             diktatConfigFile = project.file("../diktat-analysis.yml")
-            reporterType = "json"
+            reporter = "json"
             output = "some.txt"
         }
     }
@@ -106,38 +106,10 @@ class DiktatJavaExecTaskTest {
         ) {
             inputs { exclude("*") }
             diktatConfigFile = project.file("../diktat-analysis.yml")
-            reporterType = "json"
+            reporter = "json"
         }
     }
 
-    @Test
-    fun `check command line has custom reporter type with output`() {
-        assertCommandLineEquals(
-            listOf(null, "--reporter=customName,artifact=customPath")
-        ) {
-            inputs { exclude("*") }
-            diktatConfigFile = project.file("../diktat-analysis.yml")
-            reporterType = "custom:customName:customPath"
-        }
-    }
-
-    @Test
-    fun `check that project has html dependency`() {
-        val task = project.registerDiktatTask {
-            inputs { exclude("*") }
-            diktatConfigFile = project.file("../diktat-analysis.yml")
-            reporterType = "html"
-        }
-
-        Assertions.assertTrue(
-            project
-                .configurations
-                .getByName("diktat")
-                .dependencies
-                .any { it.name == "ktlint-reporter-html" })
-        Assertions.assertEquals(File(project.projectDir.parentFile, "diktat-analysis.yml").absolutePath, task.systemProperties[DIKTAT_CONF_PROPERTY])
-    }
-
     @Test
     fun `check system property with multiproject build with default config`() {
         setupMultiProject()
diff --git a/diktat-maven-plugin/src/test/kotlin/org/cqfn/diktat/plugin/maven/DiktatMavenPluginIntegrationTest.kt b/diktat-maven-plugin/src/test/kotlin/org/cqfn/diktat/plugin/maven/DiktatMavenPluginIntegrationTest.kt
index 231273b66b..dd44337614 100644
--- a/diktat-maven-plugin/src/test/kotlin/org/cqfn/diktat/plugin/maven/DiktatMavenPluginIntegrationTest.kt
+++ b/diktat-maven-plugin/src/test/kotlin/org/cqfn/diktat/plugin/maven/DiktatMavenPluginIntegrationTest.kt
@@ -46,6 +46,7 @@ class DiktatMavenPluginIntegrationTest {
         Assertions.assertTrue(result.isFailure)
 
         val mavenLog = result.mavenLog.stdout.readText()
+
         Assertions.assertTrue(
             mavenLog.contains(Regex("""Original and formatted content differ, writing to [:\w/\\]+Test\.kt\.\.\."""))
         )