diff --git a/README.md b/README.md
index 55b1b85295..e0b71bee23 100644
--- a/README.md
+++ b/README.md
@@ -211,21 +211,15 @@ diktat {
}
```
-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):
+Also in `diktat` extension you can configure different reporters and their output. You can specify `json`, `html`, `sarif`, `plain` (default).
+If `output` is set, it should be a file path. If not set, results will be printed to stdout.
```kotlin
diktat {
// since 1.2.1 to keep in line with maven properties
reporter = "json" // "html", "json", "plain" (default), "sarif"
// before 1.2.1
// reporterType = "json" // "html", "json", "plain" (default), "sarif"
-}
-```
-You can also specify an output.
-```kotlin
-diktat {
- // since 1.2.1 (reporterType for old versions)
- reporter = "json"
output = "someFile.json"
}
```
@@ -302,7 +296,7 @@ Diktat can be run via spotless-maven-plugin since version 2.8.0
```
-## GitHub Native Integration
+## GitHub Integration
We suggest everyone to use common ["sarif"](https://docs.oasis-open.org/sarif/sarif/v2.0/sarif-v2.0.html) format as a `reporter` (`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.
@@ -337,6 +331,18 @@ mvn -B diktat:check@diktat -Ddiktat.githubActions=true
with:
sarif_file: ${{ github.workspace }}
```
+
+*Note*: `codeql-action/upload-sarif` limits the number of uploaded files at 15. If your project has more than 15 subprojects,
+the limit will be exceeded and the step will fail. To solve this issue one can merge SARIF reports.
+
+`diktat-gradle-plugin` provides this capability with `mergeDiktatReports` task. This task aggregates reports of all diktat tasks
+of all Gradle project, which produce SARIF reports, and outputs the merged report into root project's build directory. Then this single
+file can be used as an input for Github action:
+```yaml
+with:
+ sarif_file: build/reports/diktat/diktat-merged.sarif
+```
+
## Customizations via `diktat-analysis.yml`
diff --git a/diktat-gradle-plugin/build.gradle.kts b/diktat-gradle-plugin/build.gradle.kts
index 95c506dbe5..b5872f5860 100644
--- a/diktat-gradle-plugin/build.gradle.kts
+++ b/diktat-gradle-plugin/build.gradle.kts
@@ -34,6 +34,7 @@ val junitVersion = project.properties.getOrDefault("junitVersion", "5.8.1") as S
val jacocoVersion = project.properties.getOrDefault("jacocoVersion", "0.8.7") as String
dependencies {
implementation(kotlin("gradle-plugin-api"))
+ implementation("io.github.detekt.sarif4k:sarif4k:0.0.1")
implementation("org.cqfn.diktat:diktat-common:$diktatVersion") {
exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable")
diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt
index e799c81815..f0a0acbc36 100644
--- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt
+++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/DiktatGradlePlugin.kt
@@ -1,5 +1,6 @@
package org.cqfn.diktat.plugin.gradle
+import org.cqfn.diktat.plugin.gradle.tasks.configureMergeReportsTask
import generated.DIKTAT_VERSION
import generated.KTLINT_VERSION
import org.gradle.api.Plugin
@@ -26,9 +27,7 @@ class DiktatGradlePlugin : Plugin {
diktatConfigFile = project.rootProject.file("diktat-analysis.yml")
}
- // only gradle 7+ (or maybe 6.8) will embed kotlin 1.4+, kx.serialization is incompatible with kotlin 1.3, so until then we have to use JavaExec wrapper
- // FixMe: when gradle with kotlin 1.4 is out, proper configurable tasks should be added
- // configuration to provide JavaExec with correct classpath
+ // Configuration that will be used as classpath for JavaExec task.
val diktatConfiguration = project.configurations.create(DIKTAT_CONFIGURATION) { configuration ->
configuration.isVisible = false
configuration.dependencies.add(project.dependencies.create("com.pinterest:ktlint:$KTLINT_VERSION", closureOf {
@@ -47,6 +46,7 @@ class DiktatGradlePlugin : Plugin {
project.registerDiktatCheckTask(diktatExtension, diktatConfiguration, patternSet)
project.registerDiktatFixTask(diktatExtension, diktatConfiguration, patternSet)
+ project.configureMergeReportsTask(diktatExtension)
}
companion object {
@@ -70,6 +70,11 @@ class DiktatGradlePlugin : Plugin {
*/
const val DIKTAT_FIX_TASK = "diktatFix"
+ /**
+ * Name of the task that merges SARIF reports of diktat tasks
+ */
+ internal const val MERGE_SARIF_REPORTS_TASK_NAME = "mergeDiktatReports"
+
/**
* Version of JVM with more strict module system, which requires `add-opens` for kotlin compiler
*/
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 a33ede567a..abd9e6fccf 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
@@ -27,8 +27,6 @@ import org.gradle.api.tasks.util.PatternSet
import org.gradle.util.GradleVersion
import java.io.File
-import java.nio.file.Files
-import java.nio.file.Paths
import javax.inject.Inject
/**
@@ -148,27 +146,21 @@ open class DiktatJavaExecTaskBase @Inject constructor(
@Suppress("FUNCTION_BOOLEAN_PREFIX")
override fun getIgnoreFailures(): Boolean = ignoreFailuresProp.getOrElse(false)
+ @Suppress("AVOID_NULL_CHECKS")
private fun reporterFlag(diktatExtension: DiktatExtension): String = buildString {
val reporterFlag = project.createReporterFlag(diktatExtension)
append(reporterFlag)
- val isSarifReporterActive = reporterFlag.contains("sarif")
- if (isSarifReporterActive) {
+ if (isSarifReporterActive(reporterFlag)) {
// need to set user.home specially for ktlint, so it will be able to put a relative path URI in SARIF
systemProperty("user.home", project.rootDir.toString())
}
- val outFlag = when {
- // githubActions should have higher priority than a custom input
- diktatExtension.githubActions -> {
- val reportDir = Files.createDirectories(Paths.get("${project.buildDir}/reports/diktat"))
- outputs.dir(reportDir)
- ",output=${reportDir.resolve("diktat.sarif")}"
- }
- diktatExtension.output.isNotEmpty() -> ",output=${diktatExtension.output}"
- else -> ""
+ val outputFile = project.getOutputFile(diktatExtension)
+ if (outputFile != null) {
+ outputs.file(outputFile)
+ val outFlag = ",output=$outputFile"
+ append(outFlag)
}
-
- append(outFlag)
}
@Suppress("MagicNumber")
diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt
index bc5a7bf1eb..9181043b48 100644
--- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt
+++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt
@@ -8,6 +8,9 @@ package org.cqfn.diktat.plugin.gradle
import groovy.lang.Closure
import org.gradle.api.Project
+import java.io.File
+import java.nio.file.Files
+import java.nio.file.Paths
@Suppress(
"MISSING_KDOC_TOP_LEVEL",
@@ -38,18 +41,18 @@ fun Any.closureOf(action: T.() -> Unit): Closure =
KotlinClosure1(action, this, this)
/**
- * Create CLI flag to select reporter based on [diktatExtension]
+ * Create CLI flag to set reporter for ktlint based on [diktatExtension].
+ * [DiktatExtension.githubActions] should have higher priority than a custom input.
*
- * @param diktatExtension project extension of type [DiktatExtension]
- * @return CLI flag
+ * @param diktatExtension extension of type [DiktatExtension]
+ * @return CLI flag as string
*/
-internal fun Project.createReporterFlag(diktatExtension: DiktatExtension): String {
+fun Project.createReporterFlag(diktatExtension: DiktatExtension): String {
val name = diktatExtension.reporter.trim()
val validReporters = listOf("sarif", "plain", "json", "html")
val reporterFlag = when {
diktatExtension.githubActions -> {
if (diktatExtension.reporter.isNotEmpty()) {
- // githubActions should have higher priority than custom input
logger.warn("`diktat.githubActions` is set to true, so custom reporter [$name] will be ignored and SARIF reporter will be used")
}
"--reporter=sarif"
@@ -67,3 +70,27 @@ internal fun Project.createReporterFlag(diktatExtension: DiktatExtension): Strin
return reporterFlag
}
+
+/**
+ * Get destination file for Diktat report or null if stdout is used.
+ * [DiktatExtension.githubActions] should have higher priority than a custom input.
+ *
+ * @param diktatExtension extension of type [DiktatExtension]
+ * @return destination [File] or null if stdout is used
+ */
+internal fun Project.getOutputFile(diktatExtension: DiktatExtension): File? = when {
+ diktatExtension.githubActions -> {
+ val reportDir = Files.createDirectories(Paths.get("${project.buildDir}/reports/diktat"))
+ reportDir.resolve("diktat.sarif").toFile()
+ }
+ diktatExtension.output.isNotEmpty() -> file(diktatExtension.output)
+ else -> null
+}
+
+/**
+ * Whether SARIF reporter is enabled or not
+ *
+ * @param reporterFlag
+ * @return whether SARIF reporter is enabled
+ */
+internal fun isSarifReporterActive(reporterFlag: String) = reporterFlag.contains("sarif")
diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt
new file mode 100644
index 0000000000..a94725749c
--- /dev/null
+++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt
@@ -0,0 +1,101 @@
+package org.cqfn.diktat.plugin.gradle.tasks
+
+import org.cqfn.diktat.plugin.gradle.DiktatExtension
+import org.cqfn.diktat.plugin.gradle.DiktatGradlePlugin.Companion.MERGE_SARIF_REPORTS_TASK_NAME
+import org.cqfn.diktat.plugin.gradle.DiktatJavaExecTaskBase
+import org.cqfn.diktat.plugin.gradle.createReporterFlag
+import org.cqfn.diktat.plugin.gradle.getOutputFile
+import org.cqfn.diktat.plugin.gradle.isSarifReporterActive
+import io.github.detekt.sarif4k.SarifSchema210
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskExecutionException
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+/**
+ * A task to merge SARIF reports produced by diktat check / diktat fix tasks.
+ */
+abstract class SarifReportMergeTask : DefaultTask() {
+ /**
+ * Source reports that should be merged
+ */
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val input: ConfigurableFileCollection
+
+ /**
+ * Destination for the merged report
+ */
+ @get:OutputFile
+ abstract val output: RegularFileProperty
+
+ /**
+ * @throws TaskExecutionException if failed to deserialize SARIF
+ */
+ @TaskAction
+ fun mergeReports() {
+ val sarifReports = input.files
+ .filter { it.exists() }
+ .also { logger.info("Merging SARIF reports from files $it") }
+ .map {
+ try {
+ Json.decodeFromString(it.readText())
+ } catch (e: SerializationException) {
+ logger.error("Couldn't deserialize JSON: is ${it.canonicalPath} a SARIF file?")
+ throw TaskExecutionException(this, e)
+ }
+ }
+
+ if (sarifReports.isEmpty()) {
+ logger.warn("Cannot perform merging of SARIF reports because no matching files were found; " +
+ "Is SARIF reporter active?"
+ )
+ return
+ }
+
+ // All reports should contain identical metadata, so we are using the first one as a base.
+ val templateReport = sarifReports.first()
+ val allResults = sarifReports.flatMap { sarifSchema ->
+ sarifSchema.runs
+ .flatMap { it.results.orEmpty() }
+ }
+ val mergedSarif = templateReport.copy(
+ runs = listOf(templateReport.runs.first().copy(results = allResults))
+ )
+
+ output.get().asFile.writeText(Json.encodeToString(mergedSarif))
+ }
+}
+
+/**
+ * @param diktatExtension extension of type [DiktatExtension]
+ */
+internal fun Project.configureMergeReportsTask(diktatExtension: DiktatExtension) {
+ if (path == rootProject.path) {
+ tasks.register(MERGE_SARIF_REPORTS_TASK_NAME, SarifReportMergeTask::class.java) { reportMergeTask ->
+ val diktatReportsDir = "${project.buildDir}/reports/diktat"
+ val mergedReportFile = project.file("$diktatReportsDir/diktat-merged.sarif")
+ reportMergeTask.outputs.file(mergedReportFile)
+ reportMergeTask.output.set(mergedReportFile)
+ }
+ }
+ val reportMergeTaskTaskProvider = rootProject.tasks.named(MERGE_SARIF_REPORTS_TASK_NAME, SarifReportMergeTask::class.java) { reportMergeTask ->
+ if (isSarifReporterActive(createReporterFlag(diktatExtension))) {
+ getOutputFile(diktatExtension)?.let { reportMergeTask.input.from(it) }
+ reportMergeTask.shouldRunAfter(tasks.withType(DiktatJavaExecTaskBase::class.java))
+ }
+ }
+ tasks.withType(DiktatJavaExecTaskBase::class.java).configureEach { diktatJavaExecTaskBase ->
+ diktatJavaExecTaskBase.finalizedBy(reportMergeTaskTaskProvider)
+ }
+}
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 07af3cd3b9..a4d79fb08d 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
@@ -91,7 +91,7 @@ class DiktatJavaExecTaskTest {
@Test
fun `check command line has reporter type and output`() {
assertCommandLineEquals(
- listOf(null, "--reporter=json,output=some.txt")
+ listOf(null, "--reporter=json,output=${project.projectDir.resolve("some.txt")}")
) {
inputs { exclude("*") }
diktatConfigFile = project.file("../diktat-analysis.yml")
diff --git a/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt
new file mode 100644
index 0000000000..472480f89f
--- /dev/null
+++ b/diktat-gradle-plugin/src/test/kotlin/org/cqfn/diktat/plugin/gradle/ReporterSelectionTest.kt
@@ -0,0 +1,34 @@
+package org.cqfn.diktat.plugin.gradle
+
+import org.gradle.api.Project
+import org.gradle.api.tasks.util.PatternSet
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class ReporterSelectionTest {
+ private val projectBuilder = ProjectBuilder.builder()
+ private lateinit var project: Project
+
+ @BeforeEach
+ fun setUp() {
+ project = projectBuilder.build()
+ // mock kotlin sources
+ project.mkdir("src/main/kotlin")
+ project.file("src/main/kotlin/Test.kt").createNewFile()
+ project.pluginManager.apply(DiktatGradlePlugin::class.java)
+ }
+
+ @Test
+ fun `should fallback to plain reporter for unknown reporter types`() {
+ val diktatExtension = DiktatExtension(PatternSet()).apply {
+ reporter = "jsonx"
+ }
+
+ Assertions.assertEquals(
+ "--reporter=plain",
+ project.createReporterFlag(diktatExtension)
+ )
+ }
+}