-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 5 commits
15c5697
fbd7661
de88318
cd1d693
3dce90e
2c582be
37c24f9
a18d09c
a1175ee
8aaac64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} | ||
}) | ||
} | ||
} | ||
} |
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( | ||
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!") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, changed to |
||
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
|
||
} | ||
} |
There was a problem hiding this comment.
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 thisThere was a problem hiding this comment.
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 haveSarifWarnAdapter
Can't imagine better name for now, so leave as is. Will change in future PR
test
->target
changed