Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions README.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ dependencyAnalysis {
}
----

If you wish to this report exported as the SARIF output, add this to the `dependencyAnalysis` DSL:

.build.gradle.kts
[source,kotlin]
----
dependencyAnalysis {
reporting {
sarifReport(true)
}
}
----

Above will generate `build/dependency-analysis/build-health-report.sarif` file on report.

== Repositories

From 2.19.0 for releases, and 2.18.1-SNAPSHOT for snapshots, this plugin uses https://central.sonatype.com. To add this
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ dependencies {
}
implementation(libs.relocated.antlr)
implementation(libs.relocated.asm)
implementation(libs.sarif4k)

runtimeOnly(libs.kotlin.reflect) {
because("For Kotlin ABI analysis")
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ moshix = "0.30.0"
#moshix = "0.31.0"
okio = "3.16.4"
shadow = "8.3.9"
sarif4k = "0.6.0"
spock = "2.3-groovy-4.0"
truth = "1.4.5"

Expand Down Expand Up @@ -83,6 +84,7 @@ okio-bom = { module = "com.squareup.okio:okio-bom", version.ref = "okio" }
relocated-antlr = { module = "com.autonomousapps:antlr", version.ref = "antlr-shadowed" }
relocated-asm = { module = "com.autonomousapps:asm-relocated", version.ref = "asm-relocated" }
shadowGradlePlugin = { module = "com.gradleup.shadow:com.gradleup.shadow.gradle.plugin", version.ref = "shadow" }
sarif4k = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "sarif4k" }
spock = { module = "org.spockframework:spock-core", version.ref = "spock" }
truth = { module = "com.google.truth:truth", version.ref = "truth" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.autonomousapps
import com.autonomousapps.extension.AbiHandler
import com.autonomousapps.extension.DependenciesHandler
import com.autonomousapps.extension.ProjectIssueHandler
import com.autonomousapps.extension.ReportingHandler
import org.gradle.api.Action
import org.gradle.api.Project
import javax.naming.OperationNotSupportedException
Expand Down Expand Up @@ -53,6 +54,11 @@ public abstract class DependencyAnalysisSubExtension(
throw OperationNotSupportedException("Dependency bundles must be declared in the root project only")
}

/** Customize issue reports. See [ReportingHandler] for more information. */
public fun reporting(action: Action<ReportingHandler>) {
action.execute(reportingHandler)
}

internal companion object {
fun of(project: Project): DependencyAnalysisSubExtension {
return project.extensions.create(NAME, DependencyAnalysisSubExtension::class.java, project)
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/com/autonomousapps/extension/ReportingHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import javax.inject.Inject
import org.gradle.api.file.RegularFileProperty
import org.jetbrains.kotlin.konan.file.File

/**
* Customize issue reports.
Expand All @@ -31,6 +33,9 @@ public abstract class ReportingHandler @Inject constructor(private val objects:
// value of the Gradle property, which itself supplies a default value.
internal val printBuildHealth: Property<Boolean> = objects.property(Boolean::class.java)

internal val sarifReport: Property<Boolean> = objects.property(Boolean::class.java)
.convention(false)

/**
* Whether to always include the postscript, or only when the report includes failure-level issues.
*/
Expand All @@ -57,6 +62,16 @@ public abstract class ReportingHandler @Inject constructor(private val objects:
this.printBuildHealth.disallowChanges()
}

/**
* Whether to generate a .sarif file report
*/
public fun sarifReport(report: Boolean) {
if (this.sarifReport.get() != report) {
this.sarifReport.set(report)
this.sarifReport.disallowChanges()
}
}

internal fun config(): Config {
val config = objects.newInstance(Config::class.java)
config.onlyOnFailure.set(onlyOnFailure)
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ internal class NoVariantOutputPaths(private val project: Project) {
*/

val unfilteredAdvicePath = file("$ROOT_DIR/unfiltered-advice.json")
val unfilteredSourcedAdvicePath = file("$ROOT_DIR/unfiltered-sourced-advice.json")
val bundledTracesPath = file("$ROOT_DIR/bundled-traces.json")
val dependencyUsagesPath = file("$ROOT_DIR/usages-dependencies.json")
val annotationProcessorUsagesPath = file("$ROOT_DIR/usages-annotation-processors.json")
val filteredAdvicePath = file("$ROOT_DIR/final-advice.json")
val filteredSourcedAdvicePath = file("$ROOT_DIR/final-sourced-advice.json")
val consoleReportPath = file("$ROOT_DIR/project-health-report.txt")
}

Expand All @@ -111,6 +113,7 @@ internal class RootOutputPaths(private val project: Project) {
val consoleReportPath = file("$ROOT_DIR/build-health-report.txt")
val allLibsVersionsTomlPath = file("$ROOT_DIR/allLibs.versions.toml")
val shouldFailPath = file("$ROOT_DIR/should-fail.txt")
val sarifReportPath = file("$ROOT_DIR/build-health-report.sarif")

val workPlanDir = dir("$ROOT_DIR/work-plan")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal class AdvicePrinter(
fun toDeclaration(advice: Advice): String =
" ${advice.toConfiguration}${gav(advice.coordinates)}"

fun fromDeclaration(advice: Advice): String =
" ${advice.fromConfiguration}${gav(advice.coordinates)}"

fun gav(coordinates: Coordinates): String {
val quotedDep = coordinates.mapped()

Expand Down Expand Up @@ -106,4 +109,4 @@ internal class AdvicePrinter(
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) 2025. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal.advice

import com.autonomousapps.model.SourcedProjectAdvice
import io.github.detekt.sarif4k.*

internal class ProjectHealthSarifReportBuilder(
projectAdvices: Collection<SourcedProjectAdvice>,
dslKind: DslKind,
/** Customize how dependencies are printed. */
dependencyMap: ((String) -> String?)? = null,
useTypesafeProjectAccessors: Boolean,
) {

val sarif: SarifSchema210

private val advicePrinter = AdvicePrinter(dslKind, dependencyMap, useTypesafeProjectAccessors)

init {
val pluginResults = projectAdvices.flatMap { projectAdvice ->
projectAdvice.pluginAdvice.map { advice ->
val location = projectAdvice.projectBuildFile?.let { buildFile ->
Location(
physicalLocation = PhysicalLocation(
artifactLocation = ArtifactLocation(uri = buildFile),
),
)
}

Result(
locations = listOfNotNull(location),
message = Message(text = "Pluigin ${advice.redundantPlugin} should be removed: ${advice.reason}"),
ruleID = "dependencyAnalysis.Plugin"
)
}

}
val dependencyResults = projectAdvices.flatMap { projectAdvice ->
projectAdvice.dependencyAdvice.map { sourcedAdvice ->
val message: String
val ruleId: String

val advice = sourcedAdvice.advice
when {
advice.isAdd() -> {
message = "Transitive dependency ${advicePrinter.toDeclaration(advice).trim()} should be declared directly"
ruleId = "dependencyanalysis.Add"
}

advice.isRemove() -> {
message = "Unused dependency ${advicePrinter.fromDeclaration(advice).trim()} should be removed"
ruleId = "dependencyanalysis.Remove"
}

advice.isChange() -> {
message =
"Dependency ${
advicePrinter.fromDeclaration(advice).trim()
} should be modified to ${advice.toConfiguration} from ${advice.fromConfiguration}"
ruleId = "dependencyanalysis.Change"
}

advice.isChangeToRuntimeOnly() -> {
message =
"Dependency ${advicePrinter.fromDeclaration(advice).trim()} should be removed or changed to runtime-only"
ruleId = "dependencyanalysis.ChangeRuntimeOnly"
}

advice.isCompileOnly() -> {
message = "Dependency ${advicePrinter.fromDeclaration(advice).trim()} should be changed to compile-only"
ruleId = "dependencyanalysis.ChangeCompileOnly"
}

advice.isProcessor() -> {
message = "Unused annotation processor ${advicePrinter.fromDeclaration(advice).trim()} should be removed"
ruleId = "dependencyanalysis.Processpr"
}

else -> {
error("Unknown advice type: $advice")
}
}

val location = projectAdvice.projectBuildFile?.let { buildFile ->
Location(
physicalLocation = PhysicalLocation(
artifactLocation = ArtifactLocation(uri = buildFile),
region =
Region(
startLine = sourcedAdvice.buildFileDeclarationLineNumber?.toLong(),
endLine = sourcedAdvice.buildFileDeclarationLineNumber?.toLong(),
)
),
)
}

Result(
locations = listOfNotNull(location),
message = Message(text = message),
ruleID = ruleId
)
}
}

sarif = SarifSchema210(
schema = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
version = Version.The210,
runs = listOf(
Run(
results = dependencyResults + pluginResults,
tool = SARIF_TOOL,
)
)
)
}
}

private val SARIF_TOOL = Tool(
driver = ToolComponent(
guid = "f9137358-f4fb-44f2-8300-39ca0b85fb77",
Comment thread
matejdro marked this conversation as resolved.
informationURI = "https://github.com/autonomousapps/dependency-analysis-gradle-plugin",
language = "en",
name = "dependency-analysis-gradle-plugin",
rules = listOf(
ReportingDescriptor(
id = "dependencyanalysis.Add",
name = "Add",
shortDescription = MultiformatMessageString(
text = "These transitive dependencies should be declared directly"
)
),
ReportingDescriptor(
id = "dependencyanalysis.Remove",
name = "Remove",
shortDescription = MultiformatMessageString(
text = "Unused dependencies which should be removed"
)
),
ReportingDescriptor(
id = "dependencyanalysis.Change",
name = "Change",
shortDescription = MultiformatMessageString(
text = "Existing dependencies which should be modified to be as indicated"
)
),
ReportingDescriptor(
id = "dependencyanalysis.ChangeRuntimeOnly",
name = "ChangeRuntimeOnly",
shortDescription = MultiformatMessageString(
text = "Dependencies which should be removed or changed to runtime-only"
)
),
ReportingDescriptor(
id = "dependencyanalysis.ChangeCompileOnly",
name = "ChangeCompileOnly",
shortDescription = MultiformatMessageString(
text = "Dependencies which could be compile-only"
)
),
ReportingDescriptor(
id = "dependencyanalysis.Processor",
name = "Processor",
shortDescription = MultiformatMessageString(
text = "Unused annotation processors that should be removed"
)
),
ReportingDescriptor(
id = "dependencyanalysis.Plugin",
name = "Plugin",
shortDescription = MultiformatMessageString(
text = "Unused plugins that can be removed"
)
),
),
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal interface DagpArtifacts : Named {
enum class Kind : ArtifactDescription<DagpArtifacts> {
COMBINED_GRAPH,
PROJECT_HEALTH,
SOURCED_PROJECT_HEALTH,
RESOLVED_DEPS,
;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ internal class StandardTransform(
advice += Advice.ofChange(
coordinates = theRemove.coordinates,
fromConfiguration = theRemove.fromConfiguration!!,
toConfiguration = theAdd.toConfiguration!!
toConfiguration = theAdd.toConfiguration!!,
)
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/utils/utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ internal fun RegularFileProperty.getAndDelete(): File {
return file
}

/**
* Resolves the file from the property (if it is declared) and deletes its contents, then returns the file.
*/
internal fun RegularFileProperty.getAndDeleteNullable(): File? {
val file = orNull?.asFile
file?.delete()
return file
}

/**
* Resolves the file from the provider and deletes its contents, then returns the file.
*/
Expand Down
Loading