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

SarifFixAdapter #4

Merged
merged 10 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 4 additions & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:

jobs:
build_and_test:
# Temporary disable action
if: ${{ false }}
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
Expand Down Expand Up @@ -97,7 +99,8 @@ jobs:

report:
name: Publish JUnit test results
if: ${{ always() }}
# Temporary disable action
if: ${{ false }}
needs: build_and_test
runs-on: ${{ matrix.os }}

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:

jobs:
detekt_check:
# Temporary disable action
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/diktat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:

jobs:
diktat_check:
# Temporary disable action
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ fun Project.configureDiktat() {
"buildSrc/**/*.kts",
"*.kts"
)
exclude("build", "buildSrc/build")
exclude("build", "buildSrc/build", "src/**/resources/*")
} else {
include("src/**/*.kt", "*.kts", "src/**/*.kts")
exclude("build/**")
exclude("build/**", "src/**/resources/*")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@file:Suppress(
"HEADER_MISSING_OR_WRONG_COPYRIGHT",
"MISSING_KDOC_TOP_LEVEL",
"MISSING_KDOC_CLASS_ELEMENTS",
"PACKAGE_NAME_INCORRECT_PREFIX",
"PACKAGE_NAME_INCORRECT_PATH",
"FILE_INCORRECT_BLOCKS_ORDER",
"WRONG_INDENTATION",
"NO_BRACES_IN_CONDITIONALS_AND_LOOPS",
)

// Copied from https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/jvm/tasks/KotlinJvmTest.kt
// which needs to be recompiled with a newer Gradle API to address https://youtrack.jetbrains.com/issue/KT-54634

/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/

package org.jetbrains.kotlin.gradle.targets.jvm.tasks

import org.gradle.api.internal.tasks.testing.JvmTestExecutionSpec
import org.gradle.api.internal.tasks.testing.TestDescriptorInternal
import org.gradle.api.internal.tasks.testing.TestExecuter
import org.gradle.api.internal.tasks.testing.TestResultProcessor
import org.gradle.api.internal.tasks.testing.TestStartEvent
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.testing.Test

@CacheableTask
open class KotlinJvmTest : Test() {
@Input
@Optional
var targetName: String? = null

override fun createTestExecuter(): TestExecuter<JvmTestExecutionSpec> =
if (targetName != null) Executor(
super.createTestExecuter(),
targetName!!
)
else super.createTestExecuter()

class Executor(
private val delegate: TestExecuter<JvmTestExecutionSpec>,
private val targetName: String
) : TestExecuter<JvmTestExecutionSpec> by delegate {
override fun execute(testExecutionSpec: JvmTestExecutionSpec, testResultProcessor: TestResultProcessor) {
delegate.execute(testExecutionSpec, object : TestResultProcessor by testResultProcessor {
override fun started(test: TestDescriptorInternal, event: TestStartEvent) {
val myTest = object : TestDescriptorInternal by test {
override fun getDisplayName(): String = "${test.displayName}[$targetName]"
override fun getClassName(): String? = test.className?.replace('$', '.')
override fun getClassDisplayName(): String? = test.classDisplayName?.replace('$', '.')
}
testResultProcessor.started(myTest, event)
}
})
}
}
}
5 changes: 1 addition & 4 deletions fixpatches/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ plugins {
id("com.saveourtool.sarifutils.buildutils.kotlin-library")
}

application {
mainClass.set("com.saveourtool.sarifutils.cli.MainKt")
}

kotlin {
val os = getCurrentOperatingSystem()

Expand All @@ -24,6 +20,7 @@ kotlin {
api(libs.okio)
implementation(libs.kotlinx.serialization.json)
implementation(libs.sarif4k)
implementation(libs.multiplatform.diff)
}
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package com.saveourtool.sarifutils.cli.adapter

import com.saveourtool.sarifutils.cli.config.FileReplacements
import com.saveourtool.sarifutils.cli.config.RuleReplacements
import com.saveourtool.sarifutils.cli.files.createTempDir
import com.saveourtool.sarifutils.cli.files.fs
import com.saveourtool.sarifutils.cli.files.readFile
import com.saveourtool.sarifutils.cli.files.readLines
import io.github.detekt.sarif4k.Replacement

import io.github.detekt.sarif4k.Run
import io.github.detekt.sarif4k.SarifSchema210
import okio.Path
import okio.Path.Companion.toPath

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

/**
* Adapter for applying sarif fix object replacements to the corresponding test files
*
* @param sarifFile path to the sarif file with fix object replacements
* @param testFiles list of the test file, to which above fixes need to be applied
*/
class SarifFixAdapter(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it an adapter?

And also why do we need "testFiles"?

Forget word 'test'. It is targetFile or something like this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I name this by analogy from save-cli, like we have SarifWarnAdapter
Can't imagine better name for now, so leave as is. Will change in future PR

test -> target changed

private val sarifFile: Path,
private val testFiles: List<Path>
) {
private val tmpDir = fs.createTempDir(SarifFixAdapter::class.simpleName!!)

/**
* Main entry for processing and applying fixes from sarif file into the test files
*
* @return list of files with applied fixes
*/
fun process(): List<Path> {
val sarifSchema210: SarifSchema210 = Json.decodeFromString(
fs.readFile(sarifFile)
)
// A run object describes a single run of an analysis tool and contains the output of that run.
val processedFiles = sarifSchema210.runs.asSequence().flatMapIndexed { index, run ->
val runReplacements: List<RuleReplacements> = extractFixObjects(run)
if (runReplacements.isEmpty()) {
println("Run #$index have no `fix object` section!")
nulls marked this conversation as resolved.
Show resolved Hide resolved
emptyList()
} else {
applyReplacementsToFiles(runReplacements, testFiles)
}
}
return processedFiles.toList()
}

/**
* Collect all fix objects into the list from sarif file
*
* @param run describes a single run of an analysis tool, and contains the reported output of that run
* @return list of replacements for all files from single [run]
*/
fun extractFixObjects(run: Run): List<RuleReplacements> {
nulls marked this conversation as resolved.
Show resolved Hide resolved
if (!run.isFixObjectExist()) {
nulls marked this conversation as resolved.
Show resolved Hide resolved
return emptyList()
}
// A result object describes a single result detected by an analysis tool.
// Each result is produced by the evaluation of a rule.
return run.results?.asSequence()
?.map { result ->
// A fix object represents a proposed fix for the problem indicated by the result.
// It specifies a set of artifacts to modify.
// For each artifact, it specifies regions to remove, and provides new content to insert.
result.fixes?.flatMap { fix ->
fix.artifactChanges.map { artifactChange ->
// TODO: What if uri is not provided? Could it be?
val filePath = artifactChange.artifactLocation.uri!!.toPath()
petertrr marked this conversation as resolved.
Show resolved Hide resolved
val replacements = artifactChange.replacements
FileReplacements(filePath, replacements)
}
} ?: emptyList()
}
?.toList() ?: emptyList()
}

private fun Run.isFixObjectExist(): Boolean = this.results?.any { result ->
result.fixes != null
0x6675636b796f75676974687562 marked this conversation as resolved.
Show resolved Hide resolved
} ?: false

/**
* Apply fixes from single run to the test files
*
* @param runReplacements list of replacements from all rules
* @param testFiles list of test files
*/
private fun applyReplacementsToFiles(runReplacements: List<RuleReplacements>, testFiles: List<Path>): List<Path> = runReplacements.flatMap { ruleReplacements ->
val filteredRuleReplacements = filterRuleReplacements(ruleReplacements)
filteredRuleReplacements?.mapNotNull { fileReplacements ->
val testFile = testFiles.find {
val fullPathOfFileFromSarif = if (!fileReplacements.filePath.isAbsolute) {
fs.canonicalize(sarifFile.parent!! / fileReplacements.filePath)
} else {
fileReplacements.filePath
}
fs.canonicalize(it) == fullPathOfFileFromSarif
}
if (testFile == null) {
println("Couldn't find appropriate test file on the path ${fileReplacements.filePath}, which provided in Sarif!")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test files? what is the relation of test files to a separate sarif library?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, changed to targetFiles

null
} else {
applyReplacementsToSingleFile(testFile, fileReplacements.replacements)
}
} ?: emptyList()
}

/**
* If there are several fixes in one file on the same line by different rules, take only the first one
*
* @param ruleReplacements list of replacements by all rules
* @return filtered list of replacements by all rules
*/
private fun filterRuleReplacements(ruleReplacements: RuleReplacements?): RuleReplacements? {
nulls marked this conversation as resolved.
Show resolved Hide resolved
// group replacements for each file by all rules
val listOfAllReplacementsForEachFile = ruleReplacements?.groupBy { fileReplacement ->
fileReplacement.filePath.toString()
}?.values

return listOfAllReplacementsForEachFile?.flatMap { fileReplacements ->
// distinct replacements from all rules for each file by `startLine`,
// i.e., take only first of possible fixes for each line
fileReplacements.distinctBy { fileReplacement ->
petertrr marked this conversation as resolved.
Show resolved Hide resolved
fileReplacement.replacements.map { replacement ->
replacement.deletedRegion.startLine
}
}
}
}

/**
* Create copy of the test file and apply fixes from sarif
*
* @param testFile test file which need to be fixed
* @param replacements corresponding replacements for [testFile]
* @return file with applied fixes
*/
private fun applyReplacementsToSingleFile(testFile: Path, replacements: List<Replacement>): Path {
val testFileCopy = tmpDir.resolve(testFile.name)
// If file doesn't exist, fill it with original data
// Otherwise, that's mean, that we already made some changes to it (by other rules),
nulls marked this conversation as resolved.
Show resolved Hide resolved
// so continue to work with modified file
if (!fs.exists(testFileCopy)) {
fs.copy(testFile, testFileCopy)
}
val fileContent = fs.readLines(testFileCopy).toMutableList()

replacements.forEach { replacement ->
val startLine = replacement.deletedRegion.startLine!!.toInt() - 1
val startColumn = replacement.deletedRegion.startColumn!!.toInt() - 1
val endColumn = replacement.deletedRegion.endColumn!!.toInt() - 1
val insertedContent = replacement.insertedContent?.text

insertedContent?.let {
fileContent[startLine] = fileContent[startLine].replaceRange(startColumn, endColumn, it)
petertrr marked this conversation as resolved.
Show resolved Hide resolved
} ?: run {
fileContent[startLine] = fileContent[startLine].removeRange(startColumn, endColumn)
}
}
fs.write(testFileCopy) {
fileContent.forEach { line ->
writeUtf8(line + '\n')
}
}
return testFileCopy
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.saveourtool.sarifutils.cli.config

import io.github.detekt.sarif4k.Replacement
import okio.Path

/**
* The list of all replacements from one rule (for different files)
*/
typealias RuleReplacements = List<FileReplacements>
0x6675636b796f75676974687562 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @property filePath path to the file
* @property replacements list of artifact changes for this [file]
*/
data class FileReplacements(
val filePath: Path,
val replacements: List<Replacement>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Utility methods to work with file system
*/

package com.saveourtool.sarifutils.cli.files

import okio.FileSystem
import okio.Path
import kotlin.random.Random

expect val fs: FileSystem

/**
* @param path a path to a file
* @return list of strings from the file
*/
fun FileSystem.readLines(path: Path): List<String> = this.read(path) {
generateSequence { readUtf8Line() }.toList()
}

/**
* @param path a path to a file
* @return string from the file
*/
fun FileSystem.readFile(path: Path): String = this.read(path) {
this.readUtf8()
}

/**
* Create a temporary directory
*
* @param prefix will be prepended to directory name
* @return a [Path] representing the created directory
*/
fun FileSystem.createTempDir(prefix: String = "sarifutils-tmp"): Path {
nulls marked this conversation as resolved.
Show resolved Hide resolved
val dirName = "$prefix-${Random.nextInt()}"
return (FileSystem.SYSTEM_TEMPORARY_DIRECTORY / dirName).also {
createDirectory(it)
nulls marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading