From df93951e33975bf4a68f2428022ca9d78d708be6 Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Mon, 19 May 2025 15:45:50 -0400 Subject: [PATCH 1/8] Add sarif support to Invert using sarif4k --- invert-gradle-plugin/build.gradle.kts | 1 + .../internal/CollectedStatAggregator.kt | 11 + .../invert/internal/InvertFileUtils.kt | 1 + .../internal/models/InvertPluginFileKey.kt | 1 + .../internal/report/InvertReportWriter.kt | 13 + .../report/sarif/InvertSarifReportWriter.kt | 297 ++++++++++++++++++ 6 files changed, 324 insertions(+) create mode 100644 invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt diff --git a/invert-gradle-plugin/build.gradle.kts b/invert-gradle-plugin/build.gradle.kts index cadf2ab..9bda3e0 100644 --- a/invert-gradle-plugin/build.gradle.kts +++ b/invert-gradle-plugin/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.coroutines.core) + implementation("io.github.detekt.sarif4k:sarif4k:0.6.0") testImplementation(libs.kotlin.test) } diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/CollectedStatAggregator.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/CollectedStatAggregator.kt index cbaa9f0..5fbc657 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/CollectedStatAggregator.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/CollectedStatAggregator.kt @@ -7,6 +7,7 @@ import com.squareup.invert.StatCollector import com.squareup.invert.internal.models.CollectedStatsForProject import com.squareup.invert.internal.models.InvertCombinedCollectedData import com.squareup.invert.internal.report.json.InvertJsonReportWriter +import com.squareup.invert.internal.report.sarif.InvertSarifReportWriter import com.squareup.invert.models.ExtraDataType import com.squareup.invert.models.ExtraMetadata import com.squareup.invert.models.ModulePath @@ -99,6 +100,16 @@ object CollectedStatAggregator { values = allCodeReferencesForStatWithProjectPathExtra ) ) + + InvertSarifReportWriter.writeToSarifReport( + description = "All CodeReferences for ${statMetadata.key}", + fileName = InvertFileUtils.outputFile( + File(reportOutputConfig.invertReportDirectory, "sarif"), + "code_references_${statMetadata.key}.sarif" + ), + metadata = statMetadata, + values = allCodeReferencesForStatWithProjectPathExtra + ) } } } diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/InvertFileUtils.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/InvertFileUtils.kt index 7d72923..1eb441d 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/InvertFileUtils.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/InvertFileUtils.kt @@ -13,6 +13,7 @@ object InvertFileUtils { const val INVERT_FOLDER_NAME = "invert" const val JS_FOLDER_NAME = "js" const val JSON_FOLDER_NAME = "json" + const val SARIF_FOLDER_NAME = "sarif" val REPORTS_SLASH_INVERT_PATH = REPORTS_FOLDER_NAME.addSlashAnd(INVERT_FOLDER_NAME) diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/models/InvertPluginFileKey.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/models/InvertPluginFileKey.kt index 64e333e..594d3d7 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/models/InvertPluginFileKey.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/models/InvertPluginFileKey.kt @@ -15,5 +15,6 @@ enum class InvertPluginFileKey( OWNERS("owners.json", "Owners"), METADATA("metadata.json", "Metadata"), STATS("stats.json", "Stats"), + STATS_SARIF("stats.sarif", "Stats"), STAT_TOTALS("stat_totals.json", "Stat Totals"), } diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt index 4f9f2b6..79fcbe1 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt @@ -9,6 +9,7 @@ import com.squareup.invert.internal.report.js.InvertJsReportUtils import com.squareup.invert.internal.report.js.InvertJsReportUtils.computeGlobalTotals import com.squareup.invert.internal.report.js.InvertJsReportWriter import com.squareup.invert.internal.report.json.InvertJsonReportWriter +import com.squareup.invert.internal.report.sarif.InvertSarifReportWriter import com.squareup.invert.logging.InvertLogger import com.squareup.invert.models.DependencyId import com.squareup.invert.models.ModulePath @@ -61,6 +62,18 @@ class InvertReportWriter( historicalData = historicalDataWithCurrent, ) + // SARIF Report + InvertSarifReportWriter(invertLogger, rootBuildReportsDir).createInvertSarifReport( + reportMetadata = reportMetadata, + allConfigurationsData = collectedConfigurations, + allProjectsDependencyData = collectedDependencies, + allProjectsStatsData = allProjectsStatsData, + allPluginsData = collectedPlugins, + allOwnersData = collectedOwners, + globalStats = globalStats, + historicalData = historicalDataWithCurrent, + ) + // HTML/JS Report InvertJsReportWriter(invertLogger, rootBuildReportsDir).createInvertHtmlReport( reportMetadata = reportMetadata, diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt new file mode 100644 index 0000000..7668b57 --- /dev/null +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt @@ -0,0 +1,297 @@ +package com.squareup.invert.internal.report.sarif + +import com.squareup.invert.internal.InvertFileUtils +import com.squareup.invert.internal.models.CollectedConfigurationsForProject +import com.squareup.invert.internal.models.CollectedDependenciesForProject +import com.squareup.invert.internal.models.CollectedOwnershipForProject +import com.squareup.invert.internal.models.CollectedPluginsForProject +import com.squareup.invert.internal.models.InvertPluginFileKey +import com.squareup.invert.internal.report.json.InvertJsonReportWriter.Companion.writeJsonFile +import com.squareup.invert.logging.InvertLogger +import com.squareup.invert.models.ModulePath +import com.squareup.invert.models.Stat +import com.squareup.invert.models.StatKey +import com.squareup.invert.models.StatMetadata +import com.squareup.invert.models.js.HistoricalData +import com.squareup.invert.models.js.MetadataJsReportModel +import com.squareup.invert.models.js.StatTotalAndMetadata +import com.squareup.invert.models.js.StatsJsReportModel +import io.github.detekt.sarif4k.ArtifactLocation +import io.github.detekt.sarif4k.Location +import io.github.detekt.sarif4k.Message +import io.github.detekt.sarif4k.MultiformatMessageString +import io.github.detekt.sarif4k.PhysicalLocation +import io.github.detekt.sarif4k.PropertyBag +import io.github.detekt.sarif4k.Region +import io.github.detekt.sarif4k.ReportingDescriptor +import io.github.detekt.sarif4k.Result as SarifResult +import io.github.detekt.sarif4k.Run +import io.github.detekt.sarif4k.SarifSchema210 +import io.github.detekt.sarif4k.SarifSerializer +import io.github.detekt.sarif4k.Tool +import io.github.detekt.sarif4k.ToolComponent +import io.github.detekt.sarif4k.Version +import kotlinx.serialization.KSerializer +import java.io.File +import java.nio.file.Files + +/** + * Writer for generating SARIF (Static Analysis Results Interchange Format) reports for Invert. + * SARIF is a standard format for static analysis tools to report their results. + */ +class InvertSarifReportWriter( + private val logger: InvertLogger, + rootBuildReportsDir: File, +) { + private val rootBuildSarifReportsDir = File(rootBuildReportsDir, InvertFileUtils.SARIF_FOLDER_NAME) + + /** + * Writes a SARIF report to a file. + */ + private fun writeToSarif( + jsonFileKey: InvertPluginFileKey, + rulesAndResults: Map> + ) { + val sarif = createSarifSchema( + toolName = "Invert", + toolVersion = "1.0.0", + rule = rulesAndResults.keys.toList(), + results = rulesAndResults.values.flatten() + ) + + + val sarifFile = InvertFileUtils.outputFile( + directory = rootBuildSarifReportsDir, + filename = jsonFileKey.filename + ) + + Files.write(sarifFile.toPath(), SarifSerializer.toMinifiedJson(sarif).toByteArray()) + } + + /** + * Extension function to convert StatsJsReportModel to a map of SARIF rules to their results. + */ + private fun StatsJsReportModel.asSarifRulesAndResults(): Map> { + val rulesAndResults = mutableMapOf>() + + statsByModule.forEach { (modulePath, statMap) -> + statMap.forEach { (statKey, stat) -> + // Get or create the rule for this stat + val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach + + // Get or create the results list for this rule + val results = rulesAndResults.getOrPut(rule) { mutableListOf() } + + // Add results for this stat + results.addAll(stat.asSarifResult(modulePath, statKey, statInfos[statKey])) + } + } + + return rulesAndResults + } + + /** + * Creates a SARIF report for Invert statistics and metadata. + * + * @param allConfigurationsData Configuration data for all projects + * @param allProjectsDependencyData Dependency data for all projects + * @param allProjectsStatsData Statistics data for all projects + * @param allPluginsData Plugin data for all projects + * @param allOwnersData Ownership data for all projects + * @param globalStats Global statistics with metadata + * @param reportMetadata Report metadata + * @param historicalData Historical data for comparison + */ + fun createInvertSarifReport( + allProjectsStatsData: StatsJsReportModel + ) { + val rulesAndResults = allProjectsStatsData.asSarifRulesAndResults() + writeToSarif( + jsonFileKey = InvertPluginFileKey.STATS_SARIF, + rulesAndResults = rulesAndResults + ) + } + + /** + * Writes a JSON file to the specified directory. + */ + private fun writeJsonFileInDir( + jsonFileKey: InvertPluginFileKey, + serializer: KSerializer, + value: T, + ) = writeJsonFile( + logger = logger, + jsonFileKey = jsonFileKey, + jsonOutputFile = InvertFileUtils.outputFile( + directory = rootBuildSarifReportsDir, + filename = jsonFileKey.filename + ), + serializer = serializer, + value = value + ) + + /** + * Creates a SARIF result for numeric stats. + */ + private fun createNumericSarifResult(key: StatKey, module: ModulePath, details: String?, value: Int): SarifResult = + SarifResult( + ruleID = key, + message = Message(text = "details: $details, value: $value"), + locations = listOf(createLocation(module)) + ) + + /** + * Creates a SARIF result for string stats. + */ + private fun createStringSarifResult(key: StatKey, module: ModulePath, details: String?, value: String): SarifResult = + SarifResult( + ruleID = key, + message = Message(text = key), + locations = listOf(createLocation(module)) + ) + + /** + * Creates a SARIF result for boolean stats. + */ + private fun createBooleanSarifResult(key: StatKey, module: ModulePath, details: String?, value: Boolean): SarifResult = + SarifResult( + ruleID = key, + message = Message(text = key), + locations = listOf(createLocation(module)) + ) + + /** + * Creates a SARIF location for a module. + */ + private fun createLocation(module: ModulePath): Location = + Location( + physicalLocation = PhysicalLocation( + artifactLocation = ArtifactLocation(uri = module) + ) + ) + + + + companion object { + private const val SARIF_FILE_NAME = "invert-report.sarif" + + fun writeToSarifReport( + values: List, + metadata: StatMetadata, + fileName: File, + description: String + ) { + val results = values.map { it.toSarifResult(metadata.key, modulePath = null, metadata) } + val rule = metadata.asReportingDescriptor() + val sarifSchema = writeSarifSchema(rule = rule, results = results) + val sarifJson = SarifSerializer.toMinifiedJson(sarifSchema) + fileName.writeText(sarifJson) + } + } +} + +/** + * Extension function to convert Stat to SARIF results. + */ +private fun Stat.asSarifResult( + module: ModulePath, + key: StatKey, + metadata: StatMetadata? +): List = when (this) { + is Stat.CodeReferencesStat -> value.map { + it.toSarifResult( + key = key, modulePath = module, metadata = metadata + ) + } + else -> emptyList() + +// is Stat.NumericStat -> listOf(createNumericSarifResult(key, module, details, value)) +// is Stat.StringStat -> listOf(createStringSarifResult(key, module, details, value)) +// is Stat.BooleanStat -> listOf(createBooleanSarifResult(key, module, details, value)) +} + +/** + * Extension function to convert CodeReference to SARIF result. + */ +private fun Stat.CodeReferencesStat.CodeReference.toSarifResult( + key: StatKey, + modulePath: ModulePath?, + metadata: StatMetadata? +): SarifResult = SarifResult( + ruleID = key, + message = Message(text = code), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + artifactLocation = ArtifactLocation(uri = filePath), + region = Region( + startLine = startLine.toLong(), + endLine = endLine.toLong(), + sourceLanguage = code?.trim() + ), + properties = PropertyBag( + extras + mapOf( + "fileType" to filePath.split(".").last() + ) + ) + ) + ) + ), + properties = PropertyBag( + mapOf( + "owner" to (owner ?: "Unknown"), + "module" to modulePath, + "uniqueId" to uniqueId, + ) + ) +) + +/** + * Extension function to convert StatMetadata to SARIF reporting descriptor. + */ +private fun StatMetadata.asReportingDescriptor(): ReportingDescriptor = + ReportingDescriptor( + id = key, + name = this.title, + fullDescription = MultiformatMessageString(markdown = description, text = description), + properties = PropertyBag( + mapOf( + "description" to description, + "extras" to extras, + "category" to category, + "title" to title + ) + ) + ) + +private fun writeSarifSchema( + rule: ReportingDescriptor, + results: List +): SarifSchema210 { + return createSarifSchema(toolName = rule.id, toolVersion = "1.0.0", rule = listOf(rule), results = results) +} + +private fun createSarifSchema( + toolName: String = "Invert", + toolVersion: String = "1.0.0", + rule: List, + results: List +): SarifSchema210 { + return SarifSchema210( + version = Version.The210, + schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + runs = listOf( + Run( + tool = Tool( + driver = ToolComponent( + name = toolName, + version = toolVersion, + rules = rule + ) + ), + results = results + ) + ) + ) + +} \ No newline at end of file From ee87848e43bda6ed8a0ebf90a617f328f9a970fe Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Mon, 19 May 2025 15:59:27 -0400 Subject: [PATCH 2/8] Cleanup Sarif report writer --- .../internal/report/InvertReportWriter.kt | 11 +-- .../report/sarif/InvertSarifReportWriter.kt | 84 ++++--------------- 2 files changed, 16 insertions(+), 79 deletions(-) diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt index 79fcbe1..b31cd17 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/InvertReportWriter.kt @@ -62,16 +62,9 @@ class InvertReportWriter( historicalData = historicalDataWithCurrent, ) - // SARIF Report + // Include all stats into one SARIF report. InvertSarifReportWriter(invertLogger, rootBuildReportsDir).createInvertSarifReport( - reportMetadata = reportMetadata, - allConfigurationsData = collectedConfigurations, - allProjectsDependencyData = collectedDependencies, - allProjectsStatsData = allProjectsStatsData, - allPluginsData = collectedPlugins, - allOwnersData = collectedOwners, - globalStats = globalStats, - historicalData = historicalDataWithCurrent, + allProjectsStatsData = allProjectsStatsData ) // HTML/JS Report diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt index 7668b57..377a1fd 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt @@ -1,10 +1,6 @@ package com.squareup.invert.internal.report.sarif import com.squareup.invert.internal.InvertFileUtils -import com.squareup.invert.internal.models.CollectedConfigurationsForProject -import com.squareup.invert.internal.models.CollectedDependenciesForProject -import com.squareup.invert.internal.models.CollectedOwnershipForProject -import com.squareup.invert.internal.models.CollectedPluginsForProject import com.squareup.invert.internal.models.InvertPluginFileKey import com.squareup.invert.internal.report.json.InvertJsonReportWriter.Companion.writeJsonFile import com.squareup.invert.logging.InvertLogger @@ -12,9 +8,6 @@ import com.squareup.invert.models.ModulePath import com.squareup.invert.models.Stat import com.squareup.invert.models.StatKey import com.squareup.invert.models.StatMetadata -import com.squareup.invert.models.js.HistoricalData -import com.squareup.invert.models.js.MetadataJsReportModel -import com.squareup.invert.models.js.StatTotalAndMetadata import com.squareup.invert.models.js.StatsJsReportModel import io.github.detekt.sarif4k.ArtifactLocation import io.github.detekt.sarif4k.Location @@ -24,6 +17,7 @@ import io.github.detekt.sarif4k.PhysicalLocation import io.github.detekt.sarif4k.PropertyBag import io.github.detekt.sarif4k.Region import io.github.detekt.sarif4k.ReportingDescriptor +import io.github.detekt.sarif4k.ReportingDescriptorReference import io.github.detekt.sarif4k.Result as SarifResult import io.github.detekt.sarif4k.Run import io.github.detekt.sarif4k.SarifSchema210 @@ -78,10 +72,10 @@ class InvertSarifReportWriter( statMap.forEach { (statKey, stat) -> // Get or create the rule for this stat val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach - + // Get or create the results list for this rule val results = rulesAndResults.getOrPut(rule) { mutableListOf() } - + // Add results for this stat results.addAll(stat.asSarifResult(modulePath, statKey, statInfos[statKey])) } @@ -93,14 +87,7 @@ class InvertSarifReportWriter( /** * Creates a SARIF report for Invert statistics and metadata. * - * @param allConfigurationsData Configuration data for all projects - * @param allProjectsDependencyData Dependency data for all projects * @param allProjectsStatsData Statistics data for all projects - * @param allPluginsData Plugin data for all projects - * @param allOwnersData Ownership data for all projects - * @param globalStats Global statistics with metadata - * @param reportMetadata Report metadata - * @param historicalData Historical data for comparison */ fun createInvertSarifReport( allProjectsStatsData: StatsJsReportModel @@ -130,48 +117,6 @@ class InvertSarifReportWriter( value = value ) - /** - * Creates a SARIF result for numeric stats. - */ - private fun createNumericSarifResult(key: StatKey, module: ModulePath, details: String?, value: Int): SarifResult = - SarifResult( - ruleID = key, - message = Message(text = "details: $details, value: $value"), - locations = listOf(createLocation(module)) - ) - - /** - * Creates a SARIF result for string stats. - */ - private fun createStringSarifResult(key: StatKey, module: ModulePath, details: String?, value: String): SarifResult = - SarifResult( - ruleID = key, - message = Message(text = key), - locations = listOf(createLocation(module)) - ) - - /** - * Creates a SARIF result for boolean stats. - */ - private fun createBooleanSarifResult(key: StatKey, module: ModulePath, details: String?, value: Boolean): SarifResult = - SarifResult( - ruleID = key, - message = Message(text = key), - locations = listOf(createLocation(module)) - ) - - /** - * Creates a SARIF location for a module. - */ - private fun createLocation(module: ModulePath): Location = - Location( - physicalLocation = PhysicalLocation( - artifactLocation = ArtifactLocation(uri = module) - ) - ) - - - companion object { private const val SARIF_FILE_NAME = "invert-report.sarif" @@ -182,8 +127,8 @@ class InvertSarifReportWriter( description: String ) { val results = values.map { it.toSarifResult(metadata.key, modulePath = null, metadata) } - val rule = metadata.asReportingDescriptor() - val sarifSchema = writeSarifSchema(rule = rule, results = results) + val rule = metadata.asReportingDescriptor(shortDescription = description) + val sarifSchema = createSarifSchemaFromResults(rule = rule, results = results) val sarifJson = SarifSerializer.toMinifiedJson(sarifSchema) fileName.writeText(sarifJson) } @@ -203,11 +148,8 @@ private fun Stat.asSarifResult( key = key, modulePath = module, metadata = metadata ) } + // No support for other stat types in SARIF else -> emptyList() - -// is Stat.NumericStat -> listOf(createNumericSarifResult(key, module, details, value)) -// is Stat.StringStat -> listOf(createStringSarifResult(key, module, details, value)) -// is Stat.BooleanStat -> listOf(createBooleanSarifResult(key, module, details, value)) } /** @@ -243,17 +185,19 @@ private fun Stat.CodeReferencesStat.CodeReference.toSarifResult( "module" to modulePath, "uniqueId" to uniqueId, ) - ) + ), + rule = ReportingDescriptorReference(id = key) ) /** * Extension function to convert StatMetadata to SARIF reporting descriptor. */ -private fun StatMetadata.asReportingDescriptor(): ReportingDescriptor = +private fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): ReportingDescriptor = ReportingDescriptor( id = key, name = this.title, fullDescription = MultiformatMessageString(markdown = description, text = description), + shortDescription = MultiformatMessageString(text = shortDescription), properties = PropertyBag( mapOf( "description" to description, @@ -264,18 +208,18 @@ private fun StatMetadata.asReportingDescriptor(): ReportingDescriptor = ) ) -private fun writeSarifSchema( +private fun createSarifSchemaFromResults( rule: ReportingDescriptor, results: List ): SarifSchema210 { - return createSarifSchema(toolName = rule.id, toolVersion = "1.0.0", rule = listOf(rule), results = results) + return createSarifSchema(toolName = rule.id, rule = listOf(rule), results = results) } private fun createSarifSchema( + rule: List, + results: List, toolName: String = "Invert", toolVersion: String = "1.0.0", - rule: List, - results: List ): SarifSchema210 { return SarifSchema210( version = Version.The210, From cee77a3fdb4e53193b5b7206fab0ef3ff22885ca Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Tue, 20 May 2025 10:37:04 -0400 Subject: [PATCH 3/8] Add determine source language private function --- .../report/sarif/InvertSarifReportWriter.kt | 111 +++++++++++------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt index 377a1fd..48b874b 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt @@ -26,6 +26,7 @@ import io.github.detekt.sarif4k.Tool import io.github.detekt.sarif4k.ToolComponent import io.github.detekt.sarif4k.Version import kotlinx.serialization.KSerializer +import org.jetbrains.annotations.VisibleForTesting import java.io.File import java.nio.file.Files @@ -62,28 +63,6 @@ class InvertSarifReportWriter( Files.write(sarifFile.toPath(), SarifSerializer.toMinifiedJson(sarif).toByteArray()) } - /** - * Extension function to convert StatsJsReportModel to a map of SARIF rules to their results. - */ - private fun StatsJsReportModel.asSarifRulesAndResults(): Map> { - val rulesAndResults = mutableMapOf>() - - statsByModule.forEach { (modulePath, statMap) -> - statMap.forEach { (statKey, stat) -> - // Get or create the rule for this stat - val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach - - // Get or create the results list for this rule - val results = rulesAndResults.getOrPut(rule) { mutableListOf() } - - // Add results for this stat - results.addAll(stat.asSarifResult(modulePath, statKey, statInfos[statKey])) - } - } - - return rulesAndResults - } - /** * Creates a SARIF report for Invert statistics and metadata. * @@ -99,24 +78,6 @@ class InvertSarifReportWriter( ) } - /** - * Writes a JSON file to the specified directory. - */ - private fun writeJsonFileInDir( - jsonFileKey: InvertPluginFileKey, - serializer: KSerializer, - value: T, - ) = writeJsonFile( - logger = logger, - jsonFileKey = jsonFileKey, - jsonOutputFile = InvertFileUtils.outputFile( - directory = rootBuildSarifReportsDir, - filename = jsonFileKey.filename - ), - serializer = serializer, - value = value - ) - companion object { private const val SARIF_FILE_NAME = "invert-report.sarif" @@ -126,6 +87,11 @@ class InvertSarifReportWriter( fileName: File, description: String ) { + if (!fileName.exists()) { + fileName.parentFile.mkdirs() + fileName.createNewFile() + } + val results = values.map { it.toSarifResult(metadata.key, modulePath = null, metadata) } val rule = metadata.asReportingDescriptor(shortDescription = description) val sarifSchema = createSarifSchemaFromResults(rule = rule, results = results) @@ -138,7 +104,8 @@ class InvertSarifReportWriter( /** * Extension function to convert Stat to SARIF results. */ -private fun Stat.asSarifResult( +@VisibleForTesting +fun Stat.asSarifResult( module: ModulePath, key: StatKey, metadata: StatMetadata? @@ -155,7 +122,8 @@ private fun Stat.asSarifResult( /** * Extension function to convert CodeReference to SARIF result. */ -private fun Stat.CodeReferencesStat.CodeReference.toSarifResult( +@VisibleForTesting +fun Stat.CodeReferencesStat.CodeReference.toSarifResult( key: StatKey, modulePath: ModulePath?, metadata: StatMetadata? @@ -169,7 +137,7 @@ private fun Stat.CodeReferencesStat.CodeReference.toSarifResult( region = Region( startLine = startLine.toLong(), endLine = endLine.toLong(), - sourceLanguage = code?.trim() + sourceLanguage = determineSourceLanguage(filePath) ), properties = PropertyBag( extras + mapOf( @@ -189,10 +157,34 @@ private fun Stat.CodeReferencesStat.CodeReference.toSarifResult( rule = ReportingDescriptorReference(id = key) ) +/** + * Extension function to convert StatsJsReportModel to a map of SARIF rules to their results. + */ +@VisibleForTesting +fun StatsJsReportModel.asSarifRulesAndResults(): Map> { + val rulesAndResults = mutableMapOf>() + + statsByModule.forEach { (modulePath, statMap) -> + statMap.forEach { (statKey, stat) -> + // Get or create the rule for this stat + val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach + + // Get or create the results list for this rule + val results = rulesAndResults.getOrPut(rule) { mutableListOf() } + + // Add results for this stat + results.addAll(stat.asSarifResult(modulePath, statKey, statInfos[statKey])) + } + } + + return rulesAndResults +} + /** * Extension function to convert StatMetadata to SARIF reporting descriptor. */ -private fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): ReportingDescriptor = +@VisibleForTesting +fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): ReportingDescriptor = ReportingDescriptor( id = key, name = this.title, @@ -208,7 +200,8 @@ private fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): R ) ) -private fun createSarifSchemaFromResults( +@VisibleForTesting +fun createSarifSchemaFromResults( rule: ReportingDescriptor, results: List ): SarifSchema210 { @@ -237,5 +230,33 @@ private fun createSarifSchema( ) ) ) +} +/** + * Simple function to determine the source language based on the file path. + */ +private fun determineSourceLanguage(filePath: String): String { + return when { + filePath.endsWith(".kt") -> "kotlin" + filePath.endsWith(".java") -> "java" + filePath.endsWith(".js") -> "javascript" + filePath.endsWith(".ts") -> "typescript" + filePath.endsWith(".swift") -> "swift" + filePath.endsWith(".m") -> "objective-c" + filePath.endsWith(".mm") -> "objective-c++" + filePath.endsWith(".c") -> "c" + filePath.endsWith(".cpp") -> "c++" + filePath.endsWith(".h") -> "c-header" + filePath.endsWith(".hpp") -> "c++-header" + filePath.endsWith(".py") -> "python" + filePath.endsWith(".rb") -> "ruby" + filePath.endsWith(".go") -> "go" + filePath.endsWith(".php") -> "php" + filePath.endsWith(".html") -> "html" + filePath.endsWith(".css") -> "css" + filePath.endsWith(".xml") -> "xml" + filePath.endsWith(".json") -> "json" + filePath.endsWith(".yaml") || filePath.endsWith(".yml") -> "yaml" + else -> "unknown" + } } \ No newline at end of file From 606dd7ef0a95fa9c57c286b4a23b90c747931d5d Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Tue, 20 May 2025 10:37:23 -0400 Subject: [PATCH 4/8] Add tests for InvertSarifRerportWriter --- .../sarif/InvertSarifReportWriterTest.kt | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt diff --git a/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt b/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt new file mode 100644 index 0000000..a081640 --- /dev/null +++ b/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt @@ -0,0 +1,140 @@ +package com.squareup.invert.internal.report.sarif + +import com.squareup.invert.internal.InvertFileUtils +import com.squareup.invert.internal.models.InvertPluginFileKey +import com.squareup.invert.logging.InvertLogger +import com.squareup.invert.models.ModulePath +import com.squareup.invert.models.OwnerName +import com.squareup.invert.models.Stat +import com.squareup.invert.models.StatDataType +import com.squareup.invert.models.StatKey +import com.squareup.invert.models.StatMetadata +import com.squareup.invert.models.js.StatsJsReportModel +import io.github.detekt.sarif4k.ReportingDescriptor +import org.gradle.internal.impldep.org.testng.Assert.assertEquals +import io.github.detekt.sarif4k.Result as SarifResult +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import java.io.File +import java.nio.file.Files + +class InvertSarifReportWriterTest { + + private val testDir = File("build/test-reports") + private val logger = object : InvertLogger { + override fun lifecycle(message: String) {} + override fun info(message: String) {} + override fun warn(message: String) {} + } + + @Test + fun `test createInvertSarifReport generates correct SARIF file`() { + // Given + val writer = InvertSarifReportWriter(logger, testDir) + val statsData = createTestStatsData() + + // When + writer.createInvertSarifReport(statsData) + + // Then + val sarifFile = File(testDir, InvertFileUtils.SARIF_FOLDER_NAME + "/" + InvertPluginFileKey.STATS_SARIF.filename) + assertTrue(sarifFile.exists(), "SARIF file should be created") + val content = sarifFile.readText() + assertTrue(content.contains("\"name\":\"Invert\""), "Should contain tool name") + assertTrue(content.contains("\"version\":\"1.0.0\""), "Should contain tool version") + } + + @Test + fun `test asSarifRulesAndResults generates correct rules and results`() { + // Given + val statsData = createTestStatsData() + val writer = InvertSarifReportWriter(logger, testDir) + + // When + val rulesAndResults = statsData.asSarifRulesAndResults() + + // Then + assertEquals(1, rulesAndResults.size, "Should have one rule") + val (rule, results) = rulesAndResults.entries.first() + assertEquals("test_stat", rule.id, "Rule ID should match") + assertEquals(2, results.size, "Should have two results") + + // Verify first result + val firstResult = results[0] + assertEquals("test_stat", firstResult.ruleID, "Result rule ID should match") + assertEquals("test code", firstResult.message.text, "Result message should match") + assertEquals("test.kt", firstResult.locations!![0].physicalLocation!!.artifactLocation!!.uri, "File path should match") + } + + @Test + fun `test writeToSarifReport generates correct SARIF file for code references`() { + // Given + val testFile = File(testDir, "test.sarif") + val codeReferences = listOf( + Stat.CodeReferencesStat.CodeReference( + filePath = "test.kt", + startLine = 1, + endLine = 10, + code = "test code", + owner = "testOwner" + ) + ) + val metadata = StatMetadata( + key = "test_stat", + description = "Test Stat", + dataType = StatDataType.CODE_REFERENCES + ) + + // When + InvertSarifReportWriter.writeToSarifReport( + values = codeReferences, + metadata = metadata, + fileName = testFile, + description = "Test Description" + ) + + // Then + assertTrue(testFile.exists(), "SARIF file should be created") + val content = testFile.readText() + assertTrue(content.contains("\"id\":\"test_stat\""), "Should contain rule ID") + assertTrue(content.contains("\"text\":\"test code\""), "Should contain code snippet") + assertTrue(content.contains("\"uri\":\"test.kt\""), "Should contain file path") + } + + private fun createTestStatsData(): StatsJsReportModel { + val codeRefStatInfo = StatMetadata( + key = "test_stat", + description = "Test Stat", + dataType = StatDataType.CODE_REFERENCES + ) + + val codeReferences = listOf( + Stat.CodeReferencesStat.CodeReference( + filePath = "test.kt", + startLine = 1, + endLine = 10, + code = "test code", + owner = "testOwner" + ), + Stat.CodeReferencesStat.CodeReference( + filePath = "test2.kt", + startLine = 5, + endLine = 15, + code = "test code 2", + owner = "testOwner2" + ) + ) + + val statsByModule = mapOf( + "module1" to mapOf( + codeRefStatInfo.key to Stat.CodeReferencesStat(codeReferences) + ) + ) + + return StatsJsReportModel( + statInfos = mapOf(codeRefStatInfo.key to codeRefStatInfo), + statsByModule = statsByModule + ) + } +} \ No newline at end of file From c92b8d8c2dcc33c2c0f51b807ff962bebce6a531 Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Tue, 20 May 2025 10:41:47 -0400 Subject: [PATCH 5/8] Restucture into InvertSarifUtils class --- .../report/sarif/InvertSarifReportWriter.kt | 189 ++---------------- .../internal/report/sarif/InvertSarifUtils.kt | 158 +++++++++++++++ .../sarif/InvertSarifReportWriterTest.kt | 7 - 3 files changed, 178 insertions(+), 176 deletions(-) create mode 100644 invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt index 48b874b..550c970 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt @@ -2,22 +2,11 @@ package com.squareup.invert.internal.report.sarif import com.squareup.invert.internal.InvertFileUtils import com.squareup.invert.internal.models.InvertPluginFileKey -import com.squareup.invert.internal.report.json.InvertJsonReportWriter.Companion.writeJsonFile import com.squareup.invert.logging.InvertLogger -import com.squareup.invert.models.ModulePath import com.squareup.invert.models.Stat -import com.squareup.invert.models.StatKey import com.squareup.invert.models.StatMetadata import com.squareup.invert.models.js.StatsJsReportModel -import io.github.detekt.sarif4k.ArtifactLocation -import io.github.detekt.sarif4k.Location -import io.github.detekt.sarif4k.Message -import io.github.detekt.sarif4k.MultiformatMessageString -import io.github.detekt.sarif4k.PhysicalLocation -import io.github.detekt.sarif4k.PropertyBag -import io.github.detekt.sarif4k.Region import io.github.detekt.sarif4k.ReportingDescriptor -import io.github.detekt.sarif4k.ReportingDescriptorReference import io.github.detekt.sarif4k.Result as SarifResult import io.github.detekt.sarif4k.Run import io.github.detekt.sarif4k.SarifSchema210 @@ -25,8 +14,6 @@ import io.github.detekt.sarif4k.SarifSerializer import io.github.detekt.sarif4k.Tool import io.github.detekt.sarif4k.ToolComponent import io.github.detekt.sarif4k.Version -import kotlinx.serialization.KSerializer -import org.jetbrains.annotations.VisibleForTesting import java.io.File import java.nio.file.Files @@ -92,171 +79,35 @@ class InvertSarifReportWriter( fileName.createNewFile() } - val results = values.map { it.toSarifResult(metadata.key, modulePath = null, metadata) } + val results = values.map { it.toSarifResult(metadata.key, modulePath = null) } val rule = metadata.asReportingDescriptor(shortDescription = description) val sarifSchema = createSarifSchemaFromResults(rule = rule, results = results) val sarifJson = SarifSerializer.toMinifiedJson(sarifSchema) fileName.writeText(sarifJson) } - } -} - -/** - * Extension function to convert Stat to SARIF results. - */ -@VisibleForTesting -fun Stat.asSarifResult( - module: ModulePath, - key: StatKey, - metadata: StatMetadata? -): List = when (this) { - is Stat.CodeReferencesStat -> value.map { - it.toSarifResult( - key = key, modulePath = module, metadata = metadata - ) - } - // No support for other stat types in SARIF - else -> emptyList() -} -/** - * Extension function to convert CodeReference to SARIF result. - */ -@VisibleForTesting -fun Stat.CodeReferencesStat.CodeReference.toSarifResult( - key: StatKey, - modulePath: ModulePath?, - metadata: StatMetadata? -): SarifResult = SarifResult( - ruleID = key, - message = Message(text = code), - locations = listOf( - Location( - physicalLocation = PhysicalLocation( - artifactLocation = ArtifactLocation(uri = filePath), - region = Region( - startLine = startLine.toLong(), - endLine = endLine.toLong(), - sourceLanguage = determineSourceLanguage(filePath) - ), - properties = PropertyBag( - extras + mapOf( - "fileType" to filePath.split(".").last() + fun createSarifSchema( + rule: List, + results: List, + toolName: String = "Invert", + toolVersion: String = "1.0.0", + ): SarifSchema210 { + return SarifSchema210( + version = Version.The210, + schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + runs = listOf( + Run( + tool = Tool( + driver = ToolComponent( + name = toolName, + version = toolVersion, + rules = rule + ) + ), + results = results ) ) ) - ) - ), - properties = PropertyBag( - mapOf( - "owner" to (owner ?: "Unknown"), - "module" to modulePath, - "uniqueId" to uniqueId, - ) - ), - rule = ReportingDescriptorReference(id = key) -) - -/** - * Extension function to convert StatsJsReportModel to a map of SARIF rules to their results. - */ -@VisibleForTesting -fun StatsJsReportModel.asSarifRulesAndResults(): Map> { - val rulesAndResults = mutableMapOf>() - - statsByModule.forEach { (modulePath, statMap) -> - statMap.forEach { (statKey, stat) -> - // Get or create the rule for this stat - val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach - - // Get or create the results list for this rule - val results = rulesAndResults.getOrPut(rule) { mutableListOf() } - - // Add results for this stat - results.addAll(stat.asSarifResult(modulePath, statKey, statInfos[statKey])) } } - - return rulesAndResults -} - -/** - * Extension function to convert StatMetadata to SARIF reporting descriptor. - */ -@VisibleForTesting -fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): ReportingDescriptor = - ReportingDescriptor( - id = key, - name = this.title, - fullDescription = MultiformatMessageString(markdown = description, text = description), - shortDescription = MultiformatMessageString(text = shortDescription), - properties = PropertyBag( - mapOf( - "description" to description, - "extras" to extras, - "category" to category, - "title" to title - ) - ) - ) - -@VisibleForTesting -fun createSarifSchemaFromResults( - rule: ReportingDescriptor, - results: List -): SarifSchema210 { - return createSarifSchema(toolName = rule.id, rule = listOf(rule), results = results) -} - -private fun createSarifSchema( - rule: List, - results: List, - toolName: String = "Invert", - toolVersion: String = "1.0.0", -): SarifSchema210 { - return SarifSchema210( - version = Version.The210, - schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", - runs = listOf( - Run( - tool = Tool( - driver = ToolComponent( - name = toolName, - version = toolVersion, - rules = rule - ) - ), - results = results - ) - ) - ) -} - -/** - * Simple function to determine the source language based on the file path. - */ -private fun determineSourceLanguage(filePath: String): String { - return when { - filePath.endsWith(".kt") -> "kotlin" - filePath.endsWith(".java") -> "java" - filePath.endsWith(".js") -> "javascript" - filePath.endsWith(".ts") -> "typescript" - filePath.endsWith(".swift") -> "swift" - filePath.endsWith(".m") -> "objective-c" - filePath.endsWith(".mm") -> "objective-c++" - filePath.endsWith(".c") -> "c" - filePath.endsWith(".cpp") -> "c++" - filePath.endsWith(".h") -> "c-header" - filePath.endsWith(".hpp") -> "c++-header" - filePath.endsWith(".py") -> "python" - filePath.endsWith(".rb") -> "ruby" - filePath.endsWith(".go") -> "go" - filePath.endsWith(".php") -> "php" - filePath.endsWith(".html") -> "html" - filePath.endsWith(".css") -> "css" - filePath.endsWith(".xml") -> "xml" - filePath.endsWith(".json") -> "json" - filePath.endsWith(".yaml") || filePath.endsWith(".yml") -> "yaml" - else -> "unknown" - } } \ No newline at end of file diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt new file mode 100644 index 0000000..ca4b745 --- /dev/null +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt @@ -0,0 +1,158 @@ +package com.squareup.invert.internal.report.sarif + +import com.squareup.invert.internal.report.sarif.InvertSarifReportWriter.Companion.createSarifSchema +import com.squareup.invert.models.ModulePath +import com.squareup.invert.models.Stat +import com.squareup.invert.models.StatKey +import com.squareup.invert.models.StatMetadata +import com.squareup.invert.models.js.StatsJsReportModel +import io.github.detekt.sarif4k.ArtifactLocation +import io.github.detekt.sarif4k.Location +import io.github.detekt.sarif4k.Message +import io.github.detekt.sarif4k.MultiformatMessageString +import io.github.detekt.sarif4k.PhysicalLocation +import io.github.detekt.sarif4k.PropertyBag +import io.github.detekt.sarif4k.Region +import io.github.detekt.sarif4k.ReportingDescriptor +import io.github.detekt.sarif4k.ReportingDescriptorReference +import io.github.detekt.sarif4k.Result as SarifResult +import io.github.detekt.sarif4k.SarifSchema210 +import org.jetbrains.annotations.VisibleForTesting +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.plus + + +/** + * Extension function to convert Stat to SARIF results. + */ +@VisibleForTesting +fun Stat.asSarifResult( + module: ModulePath, + key: StatKey +): List = when (this) { + is Stat.CodeReferencesStat -> value.map { + it.toSarifResult( + key = key, modulePath = module + ) + } + // No support for other stat types in SARIF + else -> emptyList() +} + +/** + * Extension function to convert CodeReference to SARIF result. + */ +@VisibleForTesting +fun Stat.CodeReferencesStat.CodeReference.toSarifResult( + key: StatKey, + modulePath: ModulePath? +): SarifResult = SarifResult( + ruleID = key, + message = Message(text = code), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + artifactLocation = ArtifactLocation(uri = filePath), + region = Region( + startLine = startLine.toLong(), + endLine = endLine.toLong(), + sourceLanguage = determineSourceLanguage(filePath) + ), + properties = PropertyBag( + extras + mapOf( + "fileType" to filePath.split(".").last() + ) + ) + ) + ) + ), + properties = PropertyBag( + mapOf( + "owner" to (owner ?: "Unknown"), + "module" to modulePath, + "uniqueId" to uniqueId, + ) + ), + rule = ReportingDescriptorReference(id = key) +) + +/** + * Extension function to convert StatsJsReportModel to a map of SARIF rules to their results. + */ +@VisibleForTesting +fun StatsJsReportModel.asSarifRulesAndResults(): Map> { + val rulesAndResults = mutableMapOf>() + + statsByModule.forEach { (modulePath, statMap) -> + statMap.forEach { (statKey, stat) -> + // Get or create the rule for this stat + val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach + + // Get or create the results list for this rule + val results = rulesAndResults.getOrPut(rule) { mutableListOf() } + + // Add results for this stat + results.addAll(stat.asSarifResult(modulePath, statKey)) + } + } + + return rulesAndResults +} + +/** + * Extension function to convert StatMetadata to SARIF reporting descriptor. + */ +@VisibleForTesting +fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): ReportingDescriptor = + ReportingDescriptor( + id = key, + name = this.title, + fullDescription = MultiformatMessageString(markdown = description, text = description), + shortDescription = MultiformatMessageString(text = shortDescription), + properties = PropertyBag( + mapOf( + "description" to description, + "extras" to extras, + "category" to category, + "title" to title + ) + ) + ) + +@VisibleForTesting +fun createSarifSchemaFromResults( + rule: ReportingDescriptor, + results: List +): SarifSchema210 { + return createSarifSchema(toolName = rule.id, rule = listOf(rule), results = results) +} + +/** + * Simple function to determine the source language based on the file path. + */ +private fun determineSourceLanguage(filePath: String): String { + return when { + filePath.endsWith(".kt") -> "kotlin" + filePath.endsWith(".java") -> "java" + filePath.endsWith(".js") -> "javascript" + filePath.endsWith(".ts") -> "typescript" + filePath.endsWith(".swift") -> "swift" + filePath.endsWith(".m") -> "objective-c" + filePath.endsWith(".mm") -> "objective-c++" + filePath.endsWith(".c") -> "c" + filePath.endsWith(".cpp") -> "c++" + filePath.endsWith(".h") -> "c-header" + filePath.endsWith(".hpp") -> "c++-header" + filePath.endsWith(".py") -> "python" + filePath.endsWith(".rb") -> "ruby" + filePath.endsWith(".go") -> "go" + filePath.endsWith(".php") -> "php" + filePath.endsWith(".html") -> "html" + filePath.endsWith(".css") -> "css" + filePath.endsWith(".xml") -> "xml" + filePath.endsWith(".json") -> "json" + filePath.endsWith(".yaml") || filePath.endsWith(".yml") -> "yaml" + else -> "unknown" + } +} \ No newline at end of file diff --git a/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt b/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt index a081640..d5ac244 100644 --- a/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt +++ b/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt @@ -3,21 +3,14 @@ package com.squareup.invert.internal.report.sarif import com.squareup.invert.internal.InvertFileUtils import com.squareup.invert.internal.models.InvertPluginFileKey import com.squareup.invert.logging.InvertLogger -import com.squareup.invert.models.ModulePath -import com.squareup.invert.models.OwnerName import com.squareup.invert.models.Stat import com.squareup.invert.models.StatDataType -import com.squareup.invert.models.StatKey import com.squareup.invert.models.StatMetadata import com.squareup.invert.models.js.StatsJsReportModel -import io.github.detekt.sarif4k.ReportingDescriptor import org.gradle.internal.impldep.org.testng.Assert.assertEquals -import io.github.detekt.sarif4k.Result as SarifResult import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertTrue import java.io.File -import java.nio.file.Files class InvertSarifReportWriterTest { From 9e319432807c3351e7062fe063fe97496ee95b51 Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Tue, 27 May 2025 09:25:31 -0400 Subject: [PATCH 6/8] Cleanup formatting and adjust key names --- gradle/libs.versions.toml | 2 + invert-gradle-plugin/build.gradle.kts | 2 +- .../report/sarif/InvertSarifReportWriter.kt | 12 +++--- .../internal/report/sarif/InvertSarifUtils.kt | 43 +++++++++++++------ 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e04bca9..bbabc59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,11 +16,13 @@ jetbrains-compose = "1.6.10" ksp = "1.9.23-1.0.20" truth = "1.4.2" dokka = "1.9.20" +sarif4k = "0.6.0" [libraries] anvil-annotations = { module = "com.squareup.anvil:annotations", version.ref = "anvil" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } +detekt-sarif4k = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "sarif4k" } javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax-inject" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } diff --git a/invert-gradle-plugin/build.gradle.kts b/invert-gradle-plugin/build.gradle.kts index 9bda3e0..ae85ff2 100644 --- a/invert-gradle-plugin/build.gradle.kts +++ b/invert-gradle-plugin/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.coroutines.core) - implementation("io.github.detekt.sarif4k:sarif4k:0.6.0") + implementation(libs.detekt.sarif4k) testImplementation(libs.kotlin.test) } diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt index 550c970..c8eddd2 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriter.kt @@ -35,13 +35,10 @@ class InvertSarifReportWriter( rulesAndResults: Map> ) { val sarif = createSarifSchema( - toolName = "Invert", - toolVersion = "1.0.0", rule = rulesAndResults.keys.toList(), results = rulesAndResults.values.flatten() ) - val sarifFile = InvertFileUtils.outputFile( directory = rootBuildSarifReportsDir, filename = jsonFileKey.filename @@ -67,6 +64,9 @@ class InvertSarifReportWriter( companion object { private const val SARIF_FILE_NAME = "invert-report.sarif" + private const val SARIF_INVERT_TOOL_NAME = "invert" + private const val SARIF_INVERT_TOOL_VERSION = "1.0.0" + private const val SARIF_SCHEMA = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json" fun writeToSarifReport( values: List, @@ -89,12 +89,12 @@ class InvertSarifReportWriter( fun createSarifSchema( rule: List, results: List, - toolName: String = "Invert", - toolVersion: String = "1.0.0", + toolName: String = SARIF_INVERT_TOOL_NAME, + toolVersion: String = SARIF_INVERT_TOOL_VERSION, ): SarifSchema210 { return SarifSchema210( version = Version.The210, - schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + schema = SARIF_SCHEMA, runs = listOf( Run( tool = Tool( diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt index ca4b745..ab0a3ed 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt @@ -2,6 +2,7 @@ package com.squareup.invert.internal.report.sarif import com.squareup.invert.internal.report.sarif.InvertSarifReportWriter.Companion.createSarifSchema import com.squareup.invert.models.ModulePath +import com.squareup.invert.models.OwnerInfo import com.squareup.invert.models.Stat import com.squareup.invert.models.StatKey import com.squareup.invert.models.StatMetadata @@ -23,6 +24,17 @@ import kotlin.collections.component2 import kotlin.collections.plus +data object SarifKey { + const val OWNER = "owner" + const val MODULE = "module" + const val UNIQUE_ID = "uniqueId" + const val DESCRIPTION = "description" + const val FILE_TYPE = "fileType" + const val EXTRAS = "extras" + const val CATEGORIES = "categories" + const val TITLE = "title" +} + /** * Extension function to convert Stat to SARIF results. */ @@ -60,18 +72,18 @@ fun Stat.CodeReferencesStat.CodeReference.toSarifResult( sourceLanguage = determineSourceLanguage(filePath) ), properties = PropertyBag( - extras + mapOf( - "fileType" to filePath.split(".").last() + value = extras + mapOf( + SarifKey.FILE_TYPE to filePath.split(".").last() ) ) ) ) ), properties = PropertyBag( - mapOf( - "owner" to (owner ?: "Unknown"), - "module" to modulePath, - "uniqueId" to uniqueId, + value = mapOf( + SarifKey.OWNER to (owner ?: OwnerInfo.UNOWNED), + SarifKey.MODULE to modulePath, + SarifKey.UNIQUE_ID to uniqueId, ) ), rule = ReportingDescriptorReference(id = key) @@ -90,10 +102,15 @@ fun StatsJsReportModel.asSarifRulesAndResults(): Map Date: Tue, 27 May 2025 11:22:46 -0400 Subject: [PATCH 7/8] Lowercase invert for test case. --- .../invert/internal/report/sarif/InvertSarifReportWriterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt b/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt index d5ac244..506b5d6 100644 --- a/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt +++ b/invert-gradle-plugin/src/test/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifReportWriterTest.kt @@ -34,7 +34,7 @@ class InvertSarifReportWriterTest { val sarifFile = File(testDir, InvertFileUtils.SARIF_FOLDER_NAME + "/" + InvertPluginFileKey.STATS_SARIF.filename) assertTrue(sarifFile.exists(), "SARIF file should be created") val content = sarifFile.readText() - assertTrue(content.contains("\"name\":\"Invert\""), "Should contain tool name") + assertTrue(content.contains("\"name\":\"invert\""), "Should contain tool name") assertTrue(content.contains("\"version\":\"1.0.0\""), "Should contain tool version") } From 3f0835f6e256526b133fafce85f873311b5956b0 Mon Sep 17 00:00:00 2001 From: Manny Delgado Date: Tue, 27 May 2025 12:58:00 -0400 Subject: [PATCH 8/8] Use invert as the default rule id for all --- .../squareup/invert/internal/report/sarif/InvertSarifUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt index ab0a3ed..b17cc7a 100644 --- a/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt +++ b/invert-gradle-plugin/src/main/kotlin/com/squareup/invert/internal/report/sarif/InvertSarifUtils.kt @@ -142,7 +142,7 @@ fun createSarifSchemaFromResults( rule: ReportingDescriptor, results: List ): SarifSchema210 { - return createSarifSchema(toolName = rule.id, rule = listOf(rule), results = results) + return createSarifSchema(rule = listOf(rule), results = results) } /**