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 6 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
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 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 @@ -119,7 +117,7 @@ open class DiktatJavaExecTaskBase @Inject constructor(
addInput(it)
}

add(createReporterFlag(diktatExtension))
add(reporterFlag(diktatExtension))
}
project.logger.debug("Setting JavaExec args to $args")
}
Expand Down Expand Up @@ -148,57 +146,21 @@ open class DiktatJavaExecTaskBase @Inject constructor(
@Suppress("FUNCTION_BOOLEAN_PREFIX")
override fun getIgnoreFailures(): Boolean = ignoreFailuresProp.getOrElse(false)

private fun createReporterFlag(diktatExtension: DiktatExtension): String {
val flag: StringBuilder = StringBuilder()

private fun reporterFlag(diktatExtension: DiktatExtension): String = buildString {
// appending the flag with the reporter
setReporter(diktatExtension, flag)

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 -> ""
}

flag.append(outFlag)

return flag.toString()
}

private fun setReporter(diktatExtension: DiktatExtension, flag: java.lang.StringBuilder) {
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
project.logger.warn("`diktat.githubActions` is set to true, so custom reporter [$name] will be ignored and SARIF reporter will be used")
}
"--reporter=sarif"
}
name.isEmpty() -> {
project.logger.info("Reporter name was not set. Using 'plain' reporter")
"--reporter=plain"
}
name !in validReporters -> {
project.logger.warn("Reporter name is invalid (provided value: [$name]). Falling back to 'plain' reporter")
"--reporter=plain"
}
else -> "--reporter=$name"
}

val isSarifReporterActive = reporterFlag.contains("sarif")
if (isSarifReporterActive) {
val reporterFlag = project.createReporterFlag(diktatExtension)
append(reporterFlag)
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())
}

flag.append(reporterFlag)
val outputFile = project.getOutputFile(diktatExtension)
if (outputFile != null) {
outputs.file(outputFile)
val outFlag = ",output=${outputFile}"
append(outFlag)
}
}

@Suppress("MagicNumber")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
package org.cqfn.diktat.plugin.gradle

import groovy.lang.Closure
import org.gradle.api.Project
import org.gradle.api.Task
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths

@Suppress(
"MISSING_KDOC_TOP_LEVEL",
Expand Down Expand Up @@ -35,3 +40,40 @@ class KotlinClosure1<in T : Any?, V : Any>(
)
fun <T> Any.closureOf(action: T.() -> Unit): Closure<Any?> =
KotlinClosure1(action, this, this)

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"
}
name.isEmpty() -> {
logger.info("Reporter name was not set. Using 'plain' reporter")
"--reporter=plain"
}
name !in validReporters -> {
logger.warn("Reporter name is invalid (provided value: [$name]). Falling back to 'plain' reporter")
"--reporter=plain"
}
else -> "--reporter=$name"
}

return reporterFlag
}

fun isSarifReporterActive(reporterFlag: String) = reporterFlag.contains("sarif")

fun Project.getOutputFile(diktatExtension: DiktatExtension): File? = when {
// githubActions should have higher priority than a custom input
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.cqfn.diktat.plugin.gradle.tasks

import org.cqfn.diktat.plugin.gradle.DiktatExtension
import org.cqfn.diktat.plugin.gradle.DiktatJavaExecTaskBase
import org.cqfn.diktat.plugin.gradle.getOutputFile
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

abstract class SarifReportMergeTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val input: ConfigurableFileCollection

@get:OutputFile
abstract val output: RegularFileProperty

@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))
}
}

internal fun Project.configureMergeReportsTask(diktatExtension: DiktatExtension) {
if (path == rootProject.path) {
tasks.register("mergeDiktatReports", 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 diktatOutputFile = objects.fileProperty().convention(
// { getOutputFile(diktatExtension) }
// )
val reportMergeTaskTaskProvider = rootProject.tasks.named("mergeDiktatReports", SarifReportMergeTask::class.java) { reportMergeTask ->
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
@@ -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)
)
}
}