Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gradle-plugin: add task to merge sarif reports #1456

Merged
merged 18 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
50ef2b1
[skip ci] WIP: Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 15, 2022
8c8d2f5
[skip ci] WIP: Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 18, 2022
874ed55
Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 18, 2022
89463aa
Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 18, 2022
feffc0d
Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 19, 2022
31c1dc6
Merge branch 'master' into feature/merge-sarif-reports#1452
petertrr Jul 19, 2022
25c1854
Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 19, 2022
21ab53b
Merge remote-tracking branch 'origin/feature/merge-sarif-reports#1452…
petertrr Jul 19, 2022
44ff7f8
Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 19, 2022
2a607c3
Merge remote-tracking branch 'origin/master' into feature/merge-sarif…
petertrr Jul 20, 2022
97bc342
Merge branch 'master' into feature/merge-sarif-reports#1452
petertrr Jul 20, 2022
c22346e
Merge branch 'master' into feature/merge-sarif-reports#1452
orchestr7 Jul 22, 2022
99ca737
Merge SARIF reports in diktat-gradle-plugin
petertrr Jul 22, 2022
58a2d53
Merge remote-tracking branch 'origin/master' into feature/merge-sarif…
petertrr Jul 22, 2022
fda3372
Merge remote-tracking branch 'origin/feature/merge-sarif-reports#1452…
petertrr Jul 22, 2022
55da134
Update Utils.kt
petertrr Jul 22, 2022
2529777
Merge branch 'master' into feature/merge-sarif-reports#1452
petertrr Jul 22, 2022
9f139d3
Merge branch 'master' into feature/merge-sarif-reports#1452
petertrr Jul 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```
Expand Down Expand Up @@ -302,7 +296,7 @@ Diktat can be run via spotless-maven-plugin since version 2.8.0
```
</details>

## 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.
Expand Down Expand Up @@ -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
```

</details>

## <a name="config"></a> Customizations via `diktat-analysis.yml`
Expand Down
1 change: 1 addition & 0 deletions diktat-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,9 +27,7 @@ class DiktatGradlePlugin : Plugin<Project> {
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<ExternalModuleDependency> {
Expand All @@ -47,6 +46,7 @@ class DiktatGradlePlugin : Plugin<Project> {

project.registerDiktatCheckTask(diktatExtension, diktatConfiguration, patternSet)
project.registerDiktatFixTask(diktatExtension, diktatConfiguration, patternSet)
project.configureMergeReportsTask(diktatExtension)
}

companion object {
Expand All @@ -70,6 +70,11 @@ class DiktatGradlePlugin : Plugin<Project> {
*/
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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -38,18 +41,18 @@ fun <T> Any.closureOf(action: T.() -> Unit): Closure<Any?> =
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"
Expand All @@ -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")
Original file line number Diff line number Diff line change
@@ -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<SarifSchema210>(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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
)
}
}