diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 8ec72e4..1bfa7a8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -8,6 +8,8 @@ on: jobs: build_and_test: + # Temporary disable action + if: ${{ false }} name: Build and test runs-on: ${{ matrix.os }} strategy: @@ -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 }} diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml index 7cfcfe4..652f76b 100644 --- a/.github/workflows/detekt.yml +++ b/.github/workflows/detekt.yml @@ -7,6 +7,8 @@ on: jobs: detekt_check: + # Temporary disable action + if: ${{ false }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/diktat.yml b/.github/workflows/diktat.yml index e2ae1ae..9c9b8d6 100644 --- a/.github/workflows/diktat.yml +++ b/.github/workflows/diktat.yml @@ -7,6 +7,8 @@ on: jobs: diktat_check: + # Temporary disable action + if: ${{ false }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/DiktatConfiguration.kt b/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/DiktatConfiguration.kt index 001deae..91d2f0f 100644 --- a/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/DiktatConfiguration.kt +++ b/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/DiktatConfiguration.kt @@ -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/*") } } } diff --git a/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/JacocoConfiguration.kt b/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/JacocoConfiguration.kt index e48ad12..215ebad 100644 --- a/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/JacocoConfiguration.kt +++ b/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/JacocoConfiguration.kt @@ -46,16 +46,7 @@ fun Project.configureJacoco() { } } - // `application` plugin creates jacocoTestReport task in plugin section (this is definitely incorrect behavior) - // AFTER that in "com.saveourtool.sarifutils.buildutils.kotlin-library" we try to register this task once again and fail - // so the order of plugins in `apply` is critically important - val jacocoTestReportTask = if (project.name == "fixpatches") { - val jacocoTestReportTask by tasks.named("jacocoTestReport", configure) - jacocoTestReportTask - } else { - val jacocoTestReportTask by tasks.register("jacocoTestReport", configure) - jacocoTestReportTask - } + val jacocoTestReportTask by tasks.register("jacocoTestReport", configure) jvmTestTask.finalizedBy(jacocoTestReportTask) jacocoTestReportTask.dependsOn(jvmTestTask) diff --git a/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/kotlin-library.gradle.kts b/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/kotlin-library.gradle.kts index 3f1c698..0fe38c9 100644 --- a/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/kotlin-library.gradle.kts +++ b/buildSrc/src/main/kotlin/com/saveourtool/sarifutils/buildutils/kotlin-library.gradle.kts @@ -26,16 +26,6 @@ kotlin { } } val nativeTargets = listOf(linuxX64(), mingwX64(), macosX64()) - if (project.name == "save-common") { - // additionally, save-common should be available for JS too - // fixme: shouldn't rely on hardcoded project name here - js(BOTH).browser() - - // store yarn.lock in the root directory - rootProject.extensions.configure { - lockFileDirectory = rootProject.projectDir - } - } if (hasProperty("disableRedundantTargets") && (property("disableRedundantTargets") as String?) != "false") { // with this flag we exclude targets that are present on multiple OS to speed up build diff --git a/buildSrc/src/main/kotlin/kotlin/org/jetbrains/kotlin/gradle/targets/jvm/tasks/KotlinJvmTest.kt b/buildSrc/src/main/kotlin/kotlin/org/jetbrains/kotlin/gradle/targets/jvm/tasks/KotlinJvmTest.kt new file mode 100644 index 0000000..530af50 --- /dev/null +++ b/buildSrc/src/main/kotlin/kotlin/org/jetbrains/kotlin/gradle/targets/jvm/tasks/KotlinJvmTest.kt @@ -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 = + if (targetName != null) Executor( + super.createTestExecuter(), + targetName!! + ) + else super.createTestExecuter() + + class Executor( + private val delegate: TestExecuter, + private val targetName: String + ) : TestExecuter 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) + } + }) + } + } +} diff --git a/fixpatches/build.gradle.kts b/fixpatches/build.gradle.kts index a8b1f4b..5bd80c5 100644 --- a/fixpatches/build.gradle.kts +++ b/fixpatches/build.gradle.kts @@ -1,79 +1,18 @@ -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem -import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension - plugins { - application id("com.saveourtool.sarifutils.buildutils.kotlin-library") } -application { - mainClass.set("com.saveourtool.sarifutils.cli.MainKt") -} - kotlin { - val os = getCurrentOperatingSystem() - jvm() - registerNativeBinaries(os, this) - sourceSets { val commonMain by getting { dependencies { api(libs.okio) implementation(libs.kotlinx.serialization.json) implementation(libs.sarif4k) + implementation(libs.multiplatform.diff) } } } - - linkProperExecutable(os) -} - -/** - * @param os - * @param kotlin - * @throws GradleException - */ -fun registerNativeBinaries(os: DefaultOperatingSystem, kotlin: KotlinMultiplatformExtension) { - val saveTarget = when { - os.isWindows -> kotlin.mingwX64() - os.isLinux -> kotlin.linuxX64() - os.isMacOsX -> kotlin.macosX64() - else -> throw GradleException("Unknown operating system $os") - } - - configure(listOf(saveTarget)) { - binaries { - val name = "sarifutils-${project.version}-${this@configure.name}" - executable { - this.baseName = name - entryPoint = "com.saveourtool.sarifutils.cli.main" - } - } - } -} - -/** - * @param os - * @throws GradleException - */ -fun linkProperExecutable(os: DefaultOperatingSystem) { - val linkReleaseExecutableTaskProvider = when { - os.isLinux -> tasks.getByName("linkReleaseExecutableLinuxX64") - os.isWindows -> tasks.getByName("linkReleaseExecutableMingwX64") - os.isMacOsX -> tasks.getByName("linkReleaseExecutableMacosX64") - else -> throw GradleException("Unknown operating system $os") - } - project.tasks.register("linkReleaseExecutableMultiplatform") { - dependsOn(linkReleaseExecutableTaskProvider) - } - - // disable building of some binaries to speed up build - // possible values: `all` - build all binaries, `debug` - build only debug binaries - val enabledExecutables = if (hasProperty("enabledExecutables")) property("enabledExecutables") as String else null - if (enabledExecutables != null && enabledExecutables != "all") { - linkReleaseExecutableTaskProvider.enabled = false - } } diff --git a/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/Main.kt b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/Main.kt deleted file mode 100644 index 911ed9a..0000000 --- a/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/Main.kt +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Main entry point for CLI execution - */ - -package com.saveourtool.sarifutils.cli - -fun main(args: Array) { - println("Welcome to Sarif Fix Patches!") -} diff --git a/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/adapter/SarifFixAdapter.kt b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/adapter/SarifFixAdapter.kt new file mode 100644 index 0000000..3a69210 --- /dev/null +++ b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/adapter/SarifFixAdapter.kt @@ -0,0 +1,211 @@ +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 com.saveourtool.sarifutils.cli.utils.adaptedIsAbsolute +import com.saveourtool.sarifutils.cli.utils.getUriBaseIdForArtifactLocation +import com.saveourtool.sarifutils.cli.utils.resolveUriBaseId +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 target files + * + * @param sarifFile path to the sarif file with fix object replacements + * @param targetFiles list of the target files, to which above fixes need to be applied + */ +class SarifFixAdapter( + private val sarifFile: Path, + private val targetFiles: List +) { + private val tmpDir = createTempDir(SarifFixAdapter::class.simpleName!!) + + /** + * Main entry for processing and applying fixes from sarif file into the target files + * + * @return list of files with applied fixes + */ + fun process(): List { + val sarifSchema210: SarifSchema210 = Json.decodeFromString( + 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 = extractFixObjects(run) + if (runReplacements.isEmpty()) { + println("Run #$index have no `fix object` section!") + emptyList() + } else { + applyReplacementsToFiles(runReplacements, targetFiles) + } + } + 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] + */ + internal fun extractFixObjects(run: Run): List { + // 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.mapNotNull { artifactChange -> + val currentArtifactLocation = artifactChange.artifactLocation + if (currentArtifactLocation.uri == null) { + println("Error: Field `uri` is absent in `artifactLocation`! Ignore this artifact change") + null + } else { + val uriBaseId = resolveUriBaseId( + currentArtifactLocation.getUriBaseIdForArtifactLocation(result), + run + ) + val filePath = uriBaseId / currentArtifactLocation.uri!!.toPath() + val replacements = artifactChange.replacements + FileReplacements(filePath, replacements) + } + } + } ?: emptyList() + } + ?.toList() ?: emptyList() + } + + /** + * Apply fixes from single run to the target files + * + * @param runReplacements list of replacements from all rules + * @param targetFiles list of target files + */ + private fun applyReplacementsToFiles(runReplacements: List, targetFiles: List): List = runReplacements.flatMap { ruleReplacements -> + val filteredRuleReplacements = filterRuleReplacements(ruleReplacements) + filteredRuleReplacements.mapNotNull { fileReplacements -> + val targetFile = targetFiles.find { + val fullPathOfFileFromSarif = if (!fileReplacements.filePath.adaptedIsAbsolute()) { + fs.canonicalize(sarifFile.parent!! / fileReplacements.filePath) + } else { + fileReplacements.filePath + } + fs.canonicalize(it) == fullPathOfFileFromSarif + } + if (targetFile == null) { + println("Couldn't find appropriate target file on the path ${fileReplacements.filePath}, which provided in Sarif!") + null + } else { + applyReplacementsToSingleFile(targetFile, fileReplacements.replacements) + } + } + } + + /** + * 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 { + // group replacements for each file by all rules + val listOfAllReplacementsForEachFile = ruleReplacements.groupBy { fileReplacement -> + fileReplacement.filePath.toString() + }.values + + // distinct replacements from all rules for each file by `startLine`, + // i.e., take only first of possible fixes for each line + return listOfAllReplacementsForEachFile.map { fileReplacementsListForSingleFile -> + // since we already grouped replacements by file path, it will equal for all elements in list, can take first of them + val filePath = fileReplacementsListForSingleFile.first().filePath + + val distinctReplacements = fileReplacementsListForSingleFile.flatMap { fileReplacements -> + // map fixes from different rules into single list + fileReplacements.replacements + }.groupBy { + // group all fixes for current file by startLine + it.deletedRegion.startLine + }.flatMap { entry -> + val startLine = entry.key + val replacementsList = entry.value + + if (replacementsList.size > 1) { + println("Some of fixes for $filePath were ignored, due they refer to the same line: $startLine." + + " Only the first fix will be applied") + } + replacementsList + } + .distinctBy { + it.deletedRegion.startLine + } + FileReplacements( + filePath, + distinctReplacements + ) + } + } + + /** + * Create copy of the target file and apply fixes from sarif + * + * @param targetFile target file which need to be fixed + * @param replacements corresponding replacements for [targetFile] + * @return file with applied fixes + */ + @Suppress("TOO_LONG_FUNCTION") + private fun applyReplacementsToSingleFile(targetFile: Path, replacements: List): Path { + val targetFileCopy = tmpDir.resolve(targetFile.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), + // so continue to work with modified file + if (!fs.exists(targetFileCopy)) { + fs.copy(targetFile, targetFileCopy) + } + val fileContent = readLines(targetFileCopy).toMutableList() + + replacements.forEach { replacement -> + val startLine = replacement.deletedRegion.startLine!!.toInt() - 1 + val startColumn = replacement.deletedRegion.startColumn?.let { + it.toInt() - 1 + } + val endColumn = replacement.deletedRegion.endColumn?.let { + it.toInt() - 1 + } + val insertedContent = replacement.insertedContent?.text + + insertedContent?.let { + if (startColumn != null && endColumn != null) { + fileContent[startLine] = fileContent[startLine].replaceRange(startColumn, endColumn, it) + } else { + fileContent[startLine] = it + } + } ?: run { + if (startColumn != null && endColumn != null) { + fileContent[startLine] = fileContent[startLine].removeRange(startColumn, endColumn) + } else { + // remove all content but leave empty line, until we support https://github.com/saveourtool/sarif-utils/issues/13 + fileContent[startLine] = "\n" + } + } + } + fs.write(targetFileCopy) { + fileContent.forEach { line -> + writeUtf8(line + '\n') + } + } + return targetFileCopy + } +} diff --git a/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/config/FileReplacements.kt b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/config/FileReplacements.kt new file mode 100644 index 0000000..7092c31 --- /dev/null +++ b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/config/FileReplacements.kt @@ -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 + +/** + * @property filePath path to the file + * @property replacements list of artifact changes for this [file] + */ +data class FileReplacements( + val filePath: Path, + val replacements: List +) diff --git a/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt new file mode 100644 index 0000000..ef7f94a --- /dev/null +++ b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt @@ -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 + */ +internal fun readLines(path: Path): List = fs.read(path) { + generateSequence { readUtf8Line() }.toList() +} + +/** + * @param path a path to a file + * @return string from the file + */ +internal fun readFile(path: Path): String = fs.read(path) { + this.readUtf8() +} + +/** + * Create a temporary directory + * + * @param prefix will be prepended to directory name + * @return a [Path] representing the created directory + */ +internal fun createTempDir(prefix: String = "sarifutils-tmp"): Path { + val dirName = "$prefix-${Random.nextInt()}" + return (FileSystem.SYSTEM_TEMPORARY_DIRECTORY / dirName).also { + fs.createDirectories(it) + } +} diff --git a/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/utils/SarifUtils.kt b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/utils/SarifUtils.kt new file mode 100644 index 0000000..6bc30b8 --- /dev/null +++ b/fixpatches/src/commonMain/kotlin/com/saveourtool/sarifutils/cli/utils/SarifUtils.kt @@ -0,0 +1,94 @@ +/** + * Utility methods to work with SARIF files. + */ + +package com.saveourtool.sarifutils.cli.utils + +import io.github.detekt.sarif4k.ArtifactLocation +import io.github.detekt.sarif4k.Result +import io.github.detekt.sarif4k.Run +import okio.Path +import okio.Path.Companion.toPath + +/** + * @return string with trimmed `file://` or `file:///` + */ +internal fun String.dropFileProtocol() = substringAfter("file://") + .let { + // It is a valid format for Windows paths to look like `file:///C:/stuff` + if (it[0] == '/' && it[2] == ':') it.drop(1) else it + } + +/** + * For some reasons, okio does not take into account paths like 'C:/abc/drfd' as absolute, because of slashes `/` + * replace `/` to the `\\` if any, and call isAbsolute from okio + * + * @return whether the path is absolute + */ +@Suppress("FUNCTION_BOOLEAN_PREFIX", "ComplexCondition") +internal fun Path.adaptedIsAbsolute(): Boolean { + val stringRepresentation = this.toString().dropFileProtocol() + if (stringRepresentation.length > 2 && + (stringRepresentation.first() in 'a'..'z' || stringRepresentation.first() in 'A'..'Z') && + (stringRepresentation[1] == ':') + ) { + return stringRepresentation.replace('/', '\\').toPath().isAbsolute + } + return this.isAbsolute +} + +/** + * `uriBaseID` could be provided directly in `artifactLocation` or in corresponding field from `locations` scope in `results` scope + * + * @param result object describes a single result detected by an analysis tool. + * @return uriBaseID directly from [ArtifactLocation] or from `locations` section, corresponding to this [ArtifactLocation] + */ +internal fun ArtifactLocation.getUriBaseIdForArtifactLocation( + result: Result +): String? { + val uriBaseIdFromLocations = result.locations?.find { + it.physicalLocation?.artifactLocation?.uri == this.uri + } + ?.physicalLocation + ?.artifactLocation + ?.uriBaseID + return this.uriBaseID ?: uriBaseIdFromLocations +} + +/** + * Recursively resolve base uri: https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317498 + * + * @param uriBaseId string which indirectly specifies the absolute URI with respect to which that relative reference is interpreted + * @param run describes a single run of an analysis tool, and contains the reported output of that run + * @return resolved uriBaseId + */ +@Suppress("ReturnCount") +internal fun resolveUriBaseId(uriBaseId: String?, run: Run): Path { + // If `uriBaseID` is not absolute path, then it should be the key from `run.originalURIBaseIDS`; + // also the tool can set the uriBaseId property to the "%srcroot%" in the absence of `run.originalURIBaseIDS`, + // which have been agreed that this indicates the root of the source tree in which the file appears. + val originalUri = if (uriBaseId?.dropFileProtocol()?.toPath()?.adaptedIsAbsolute() == true) { + return uriBaseId.dropFileProtocol().toPath() + } else { + run.originalURIBaseIDS?.get(uriBaseId) ?: return ".".toPath() + } + + return if (originalUri.uri == null) { + // base uri is the root + if (originalUri.uriBaseID == null) { + ".".toPath() + // recursively resolve base uri + } else { + resolveUriBaseId(originalUri.uriBaseID!!, run) + } + } else { + val uri = originalUri.uri!!.dropFileProtocol().toPath() + // uri is required path + if (uri.adaptedIsAbsolute()) { + uri + // recursively concatenate uri with the base uri + } else { + resolveUriBaseId(originalUri.uriBaseID, run) / uri + } + } +} diff --git a/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/SarifFixAdapterTest.kt b/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/SarifFixAdapterTest.kt deleted file mode 100644 index 788bb75..0000000 --- a/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/SarifFixAdapterTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.saveourtool.sarifutils - -import io.github.detekt.sarif4k.SarifSchema210 -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json - -class SarifFixAdapterTest { - @Test - @Suppress("TOO_LONG_FUNCTION") - fun `simple check - should read SARIF report`() { - val sarif = """ - { - "version": "2.1.0", - "${'$'}schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", - "runs": [ - { - "tool": { - "driver": { - "name": "ESLint", - "informationUri": "https://eslint.org", - "rules": [ - { - "id": "no-unused-vars", - "shortDescription": { - "text": "disallow unused variables" - }, - "helpUri": "https://eslint.org/docs/rules/no-unused-vars" - } - ] - } - }, - "artifacts": [ - { - "location": { - "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" - } - } - ], - "results": [ - { - "level": "error", - "message": { - "text": "'x' is assigned a value but never used." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js", - "index": 0 - }, - "region": { - "startLine": 1, - "startColumn": 5 - } - } - } - ], - "ruleId": "no-unused-vars", - "ruleIndex": 0 - } - ] - } - ] - } - """.trimIndent() - - val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarif) - - val result = sarifSchema210.runs.first() - .results - ?.first()!! - - assertEquals(result.message.text, "'x' is assigned a value but never used.") - assertEquals(result.locations?.first()?.physicalLocation?.artifactLocation - ?.uri, "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js") - } -} diff --git a/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/adapter/SarifFixAdapterTest.kt b/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/adapter/SarifFixAdapterTest.kt new file mode 100644 index 0000000..42ad5c2 --- /dev/null +++ b/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/adapter/SarifFixAdapterTest.kt @@ -0,0 +1,451 @@ +@file:Suppress("FILE_IS_TOO_LONG") + +package com.saveourtool.sarifutils.adapter + +import com.saveourtool.sarifutils.cli.adapter.SarifFixAdapter +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.SarifSchema210 +import io.github.petertrr.diffutils.diff +import io.github.petertrr.diffutils.patch.ChangeDelta +import io.github.petertrr.diffutils.patch.Patch +import io.github.petertrr.diffutils.text.DiffRowGenerator +import okio.Path +import okio.Path.Companion.toPath + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +// FixMe: Possible problems with tests on native platforms: +// https://youtrack.jetbrains.com/issue/KT-54634/MPP-Test-Failure-causes-KotlinJvmTestExecutorexecute1-does-not-define-failure +@Suppress("TOO_LONG_FUNCTION") +class SarifFixAdapterTest { + private val diffGenerator = DiffRowGenerator( + showInlineDiffs = true, + mergeOriginalRevised = false, + inlineDiffByWord = false, + oldTag = { _, start -> if (start) "[" else "]" }, + newTag = { _, start -> if (start) "<" else ">" }, + ) + + @Test + @Suppress("TOO_LONG_FUNCTION") + fun `should read SARIF report`() { + val uri = "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" + val sarif = """ + { + "version": "2.1.0", + "${'$'}schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", + "runs": [ + { + "tool": { + "driver": { + "name": "ESLint", + "informationUri": "https://eslint.org", + "rules": [ + { + "id": "no-unused-vars", + "shortDescription": { + "text": "disallow unused variables" + }, + "helpUri": "https://eslint.org/docs/rules/no-unused-vars" + } + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "$uri" + } + } + ], + "results": [ + { + "level": "error", + "message": { + "text": "'x' is assigned a value but never used." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "$uri", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 5 + } + } + } + ], + "ruleId": "no-unused-vars", + "ruleIndex": 0 + } + ] + } + ] + } + """.trimIndent() + + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarif) + + val result = sarifSchema210.runs.first() + .results + ?.first()!! + + assertEquals("'x' is assigned a value but never used.", result.message.text) + assertEquals(uri, result.locations?.first()?.physicalLocation?.artifactLocation + ?.uri) + } + + @Test + fun `should read SARIF file`() { + val sarifFilePath = "src/commonTest/resources/sarif-fixes.sarif".toPath() + val sarifFile = readFile(sarifFilePath) + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarifFile) + + val result = sarifSchema210.runs.first() + .results + ?.first()!! + + assertEquals("[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_", result.message.text) + } + + @Test + fun `should extract SARIF fix objects`() { + val sarifFilePath = "src/commonTest/resources/sarif-fixes.sarif".toPath() + val sarifFile = readFile(sarifFilePath) + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarifFile) + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = emptyList() + ) + val results = sarifSchema210.runs.map { + sarifFixAdapter.extractFixObjects(it) + } + // Number of runs + assertEquals(1, results.size) + + // Number of fixes (rules) from first run + val numberOfFixesFromFirstRun = results.first() + assertEquals(1, numberOfFixesFromFirstRun.size) // that's mean, that it's only one fix + + // Number of first fix artifact changes (probably for several files) + val firstFixArtifactChanges = numberOfFixesFromFirstRun.first() + assertEquals(1, firstFixArtifactChanges.size) + + val firstArtifactChanges = firstFixArtifactChanges.first() + + assertEquals("src/kotlin/EnumValueSnakeCaseTest.kt".toPath(), firstArtifactChanges.filePath) + + // Number of replacements from first artifact change + assertEquals(1, firstArtifactChanges.replacements.size) + + val changes = firstArtifactChanges.replacements.first() + compareDeletedRegion(changes, 9, 5, 9, 19, "nameMyaSayR") + } + + @Test + fun `should extract SARIF fix objects 2`() { + val sarifFilePath = "src/commonTest/resources/sarif-fixes-2.sarif".toPath() + val sarifFile = readFile(sarifFilePath) + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarifFile) + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = emptyList() + ) + val results = sarifSchema210.runs.map { + sarifFixAdapter.extractFixObjects(it) + } + + // Number of runs + assertEquals(1, results.size) + + // Number of fixes (rules) from first run + val numberOfFixesFromFirstRun = results.first() + assertEquals(2, numberOfFixesFromFirstRun.size) + + // Number of first fix artifact changes (probably for several files) + val firstFixArtifactChanges = numberOfFixesFromFirstRun.first() + assertEquals(1, firstFixArtifactChanges.size) + + val firstArtifactChangesForFirstFix = firstFixArtifactChanges.first() + + assertEquals("targets/autofix/autofix.py".toPath(), firstArtifactChangesForFirstFix.filePath) + + // Number of replacements from first artifact change + assertEquals(1, firstArtifactChangesForFirstFix.replacements.size) + + val changes = firstArtifactChangesForFirstFix.replacements.first() + compareDeletedRegion(changes, 5, 3, 5, 16, " inputs.get(x) = 1") + + // =================================================================== // + + // Number of second fix artifact changes (probably for several files) + val secondFixArtifactChanges = numberOfFixesFromFirstRun.last() + assertEquals(1, secondFixArtifactChanges.size) + + val firstArtifactChangesForSecondFix = secondFixArtifactChanges.first() + + assertEquals("targets/autofix/autofix.py".toPath(), firstArtifactChangesForSecondFix.filePath) + + // Number of replacements from first artifact change + assertEquals(1, firstArtifactChangesForSecondFix.replacements.size) + + val changes2 = firstArtifactChangesForSecondFix.replacements.first() + compareDeletedRegion(changes2, 6, 3, 6, 28, " if inputs.get(x + 1) == True:") + } + + @Test + fun `should extract SARIF fix objects 3`() { + val sarifFilePath = "src/commonTest/resources/sarif-fixes-3.sarif".toPath() + val sarifFile = readFile(sarifFilePath) + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarifFile) + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = emptyList() + ) + val results = sarifSchema210.runs.map { + sarifFixAdapter.extractFixObjects(it) + } + // Number of runs + assertEquals(1, results.size) + + // Number of fixes (rules) from first run + val numberOfFixesFromFirstRun = results.first() + assertEquals(1, numberOfFixesFromFirstRun.size) // that's mean, that it's only one fix + + // Number of first fix artifact changes (probably for several files) + val firstFixArtifactChanges = numberOfFixesFromFirstRun.first() + assertEquals(2, firstFixArtifactChanges.size) + + val firstArtifactChanges = firstFixArtifactChanges.first() + + assertEquals("src/kotlin/Test1.kt".toPath(), firstArtifactChanges.filePath) + + // Number of replacements from first artifact change + assertEquals(1, firstArtifactChanges.replacements.size) + + val changes = firstArtifactChanges.replacements.first() + compareDeletedRegion(changes, 9, 5, 9, 19, "nameMyaSayR") + + // ========================================================= // + + val secondArtifactChanges = firstFixArtifactChanges.last() + + assertEquals(secondArtifactChanges.filePath, "src/kotlin/Test2.kt".toPath()) + + // Number of replacements from second artifact change + assertEquals(1, secondArtifactChanges.replacements.size) + + val changes2 = secondArtifactChanges.replacements.first() + compareDeletedRegion(changes2, 9, 5, 9, 19, "nameMyaSayR") + } + + @Test + fun `should extract SARIF fix objects 4`() { + val sarifFilePath = "src/commonTest/resources/sarif-warn-and-fixes.sarif".toPath() + val sarifFile = readFile(sarifFilePath) + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarifFile) + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = emptyList() + ) + val results = sarifSchema210.runs.map { + sarifFixAdapter.extractFixObjects(it) + } + + // Number of runs + assertEquals(1, results.size) + + // Number of fixes (rules) from first run + val numberOfFixesFromFirstRun = results.first() + assertEquals(1, numberOfFixesFromFirstRun.size) // that's mean, that it's only one fix + + // Number of first fix artifact changes (probably for several files) + val firstFixArtifactChanges = numberOfFixesFromFirstRun.first() + assertEquals(2, firstFixArtifactChanges.size) + + val firstArtifactChanges = firstFixArtifactChanges.first() + + assertEquals("needsfix/NeedsFix.cs".toPath(), firstArtifactChanges.filePath) + + // Number of replacements from first artifact change + assertEquals(1, firstArtifactChanges.replacements.size) + + val changes = firstArtifactChanges.replacements.first() + compareDeletedRegion(changes, 7, 17, null, 22, "word") + + val secondArtifactChanges = firstFixArtifactChanges.last() + + assertEquals("needsfix/NeedsFix.cs".toPath(), secondArtifactChanges.filePath) + + // Number of replacements from second artifact change + assertEquals(1, secondArtifactChanges.replacements.size) + + val changes2 = secondArtifactChanges.replacements.first() + compareDeletedRegion(changes2, 7, 17, null, 23, null) + } + + @Test + fun `sarif fix adapter test`() { + val sarifFilePath = "src/commonTest/resources/sarif-fixes.sarif".toPath() + val testFile = "src/commonTest/resources/src/kotlin/EnumValueSnakeCaseTest.kt".toPath() + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = listOf(testFile) + ) + + val processedFile = sarifFixAdapter.process().first() + + val diff = calculateDiff(testFile, processedFile) + + val expectedDelta = + """ + ChangeDelta, position 8, lines: + - [NA]me[_]M[Y]a[_s]ayR[_] + + meMaayR + """.trimIndent() + + assertEquals(expectedDelta, diff.trimIndent()) + } + + @Test + fun `sarif fix adapter test 2`() { + val sarifFilePath = "src/commonTest/resources/sarif-fixes-2.sarif".toPath() + val testFile = "src/commonTest/resources/targets/autofix/autofix.py".toPath() + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = listOf(testFile) + ) + + val processedFile = sarifFixAdapter.process().first() + + val diff = calculateDiff(testFile, processedFile) + + val expectedDelta = + """ + ChangeDelta, position 4, lines: + - inputs[[]x[]] = 1 + + < >inputs<.get(>x<)> = 1 + + + - if inputs[[]x + 1[]] == True: + + < >if inputs<.get(>x + 1<)> == True: + """.trimIndent() + + assertEquals(expectedDelta, diff.trimIndent()) + } + + @Test + fun `sarif fix adapter test 3`() { + val testFiles = listOf( + "src/commonTest/resources/src/kotlin/Test1.kt".toPath(), + "src/commonTest/resources/src/kotlin/Test2.kt".toPath() + ) + val sarifFilePath = "src/commonTest/resources/sarif-fixes-3.sarif".toPath() + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = testFiles + ) + + val processedFiles = sarifFixAdapter.process() + val firstProcessedFile = processedFiles.first() + val secondProcessedFile = processedFiles.last() + + val diff = calculateDiff(testFiles.first(), firstProcessedFile) + val expectedDelta = + """ + ChangeDelta, position 8, lines: + - [NA]me[_]M[Y]a[_s]ayR[_] + + meMaayR + """.trimIndent() + + assertEquals(diff.trimIndent(), expectedDelta) + + // ============================================================ // + + val diff2 = calculateDiff(testFiles.last(), secondProcessedFile) + val expectedDelta2 = + """ + ChangeDelta, position 8, lines: + - [NA]me[_]M[Y]a[_s]ayR[_] + + meMaayR + """.trimIndent() + + assertEquals(expectedDelta2, diff2.trimIndent()) + } + + @Test + fun `sarif fix adapter test 4`() { + val sarifFilePath = "src/commonTest/resources/sarif-warn-and-fixes.sarif".toPath() + val testFile = "src/commonTest/resources/needsfix/NeedsFix.cs".toPath() + + val sarifFixAdapter = SarifFixAdapter( + sarifFile = sarifFilePath, + targetFiles = listOf(testFile) + ) + + val processedFile = sarifFixAdapter.process().first() + + val diff = calculateDiff(testFile, processedFile) + + val expectedDelta = + """ + ChangeDelta, position 6, lines: + - // This wo[o]rd is spelled wrong. + + // This word is spelled wrong. + """.trimIndent() + + assertEquals(expectedDelta, diff.trimIndent()) + } + + @Suppress("TOO_MANY_PARAMETERS") + private fun compareDeletedRegion( + changes: Replacement, + actualStartLine: Long, + actualStartColumn: Long, + actualEndLine: Long?, + actualEndColumn: Long, + actualInsertedText: String?, + ) { + assertEquals(changes.deletedRegion.startLine, actualStartLine) + assertEquals(changes.deletedRegion.startColumn, actualStartColumn) + assertEquals(changes.deletedRegion.endLine, actualEndLine) + assertEquals(changes.deletedRegion.endColumn, actualEndColumn) + assertEquals(changes.insertedContent?.text, actualInsertedText) + } + + private fun Patch.formatToString() = deltas.joinToString("\n") { delta -> + when (delta) { + is ChangeDelta -> diffGenerator + .generateDiffRows(delta.source.lines, delta.target.lines) + .joinToString(prefix = "ChangeDelta, position ${delta.source.position}, lines:\n", separator = "\n\n") { + """-${it.oldLine} + |+${it.newLine} + |""".trimMargin() + } + else -> delta.toString() + } + } + + private fun calculateDiff(testFile: Path, processedFile: Path) = diff(readLines(testFile), readLines(processedFile)).let { patch -> + if (patch.deltas.isEmpty()) { + "" + } else { + patch.formatToString() + } + } +} diff --git a/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/utils/SarifUtilsTest.kt b/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/utils/SarifUtilsTest.kt new file mode 100644 index 0000000..623afa0 --- /dev/null +++ b/fixpatches/src/commonTest/kotlin/com/saveourtool/sarifutils/utils/SarifUtilsTest.kt @@ -0,0 +1,232 @@ +package com.saveourtool.sarifutils.utils + +import com.saveourtool.sarifutils.cli.utils.getUriBaseIdForArtifactLocation +import com.saveourtool.sarifutils.cli.utils.resolveUriBaseId + +import io.github.detekt.sarif4k.SarifSchema210 +import okio.Path +import okio.Path.Companion.toPath + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Suppress("TOO_LONG_FUNCTION") +class SarifUtilsTest { + private fun getSarif( + originalUriBaseIds: String, + uriBaseIdInArtifactLocation: String, + uriBaseIdInLocations: String, + ) = """ + { + "${'$'}schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + $originalUriBaseIds + "results": [ + { + "fixes": [ + { + "artifactChanges": [ + { + "artifactLocation": { + "uri": "src/kotlin/EnumValueSnakeCaseTest.kt"${if (uriBaseIdInArtifactLocation.isNotBlank()) "," else ""} + $uriBaseIdInArtifactLocation + }, + "replacements": [ + { + "deletedRegion": { + "endColumn": 19, + "endLine": 9, + "startColumn": 5, + "startLine": 9 + }, + "insertedContent": { + "text": "nameMyaSayR" + } + } + ] + } + ], + "description": { + "text": "[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_" + } + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/kotlin/EnumValueSnakeCaseTest.kt"${if (uriBaseIdInLocations.isNotBlank()) "," else ""} + $uriBaseIdInLocations + }, + "region": { + "endColumn": 19, + "endLine": 9, + "snippet": { + "text": "NAme_MYa_sayR_" + }, + "startColumn": 5, + "startLine": 9 + } + } + } + ], + "message": { + "text": "[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_" + }, + "ruleId": "diktat-ruleset:identifier-naming" + } + ], + "tool": { + "driver": { + "downloadUri": "https://github.com/pinterest/ktlint/releases/tag/0.42.0", + "fullName": "ktlint", + "informationUri": "https://github.com/pinterest/ktlint/", + "language": "en", + "name": "ktlint", + "organization": "pinterest", + "rules": [ + ], + "semanticVersion": "0.42.0", + "version": "0.42.0" + } + } + } + ] + } + """.trimIndent() + + @Test + fun `should resolve base uri 1`() { + val sarif = getSarif( + originalUriBaseIds = """ + "originalUriBaseIds": { + "%SRCROOT%": { + "uri": "file:///home/projects/" + } + }, + """, + uriBaseIdInArtifactLocation = "\"uriBaseId\": \"%SRCROOT%\"", + uriBaseIdInLocations = "" + ) + assertBaseUri(sarif, "/home/projects".toPath()) + } + + @Test + fun `should resolve base uri 2`() { + val sarif = getSarif( + originalUriBaseIds = """ + "originalUriBaseIds": { + "%SRCROOT%": { + "uri": "file:///home/projects/" + } + }, + """, + uriBaseIdInArtifactLocation = "", + uriBaseIdInLocations = "\"uriBaseId\": \"%SRCROOT%\"" + ) + assertBaseUri(sarif, "/home/projects".toPath()) + } + + @Test + fun `should resolve base uri 3`() { + val sarif = getSarif( + originalUriBaseIds = "", + uriBaseIdInArtifactLocation = "\"uriBaseId\": \"%SRCROOT%\"", + uriBaseIdInLocations = "" + ) + assertBaseUri(sarif, ".".toPath()) + } + + @Test + fun `should resolve base uri 4`() { + val sarif = getSarif( + originalUriBaseIds = "", + uriBaseIdInArtifactLocation = "", + uriBaseIdInLocations = "\"uriBaseId\": \"%SRCROOT%\"" + ) + assertBaseUri(sarif, ".".toPath()) + } + + @Test + fun `should resolve base uri 5`() { + val sarif = getSarif( + originalUriBaseIds = "", + uriBaseIdInArtifactLocation = "\"uriBaseId\": \"file:///C:/projects/\"", + uriBaseIdInLocations = "" + ) + assertBaseUri(sarif, "C:/projects/".toPath()) + } + + @Test + fun `should resolve base uri 6`() { + val sarif = getSarif( + originalUriBaseIds = "", + uriBaseIdInArtifactLocation = "\"uriBaseId\": \"C:/projects/\"", + uriBaseIdInLocations = "" + ) + assertBaseUri(sarif, "C:/projects".toPath()) + } + + @Test + fun `should resolve base uri 7`() { + val sarif = getSarif( + originalUriBaseIds = "", + uriBaseIdInArtifactLocation = "", + uriBaseIdInLocations = "\"uriBaseId\": \"file:///home/projects/\"" + ) + assertBaseUri(sarif, "/home/projects".toPath()) + } + + @Test + fun `should resolve base uri 8`() { + val sarif = getSarif( + originalUriBaseIds = """ + "originalUriBaseIds": { + "PROJECTROOT": { + "uri": "file:///C:/Users/Mary/code/TheProject/", + "description": { + "text": "The root directory for all project files." + } + }, + "SRCROOT": { + "uri": "src", + "uriBaseId": "PROJECTROOT", + "description": { + "text": "The root of the source tree." + } + } + } + """, + uriBaseIdInArtifactLocation = "", + uriBaseIdInLocations = "\"uriBaseId\": \"SRCROOT\"" + ) + assertBaseUri(sarif, "C:/Users/Mary/code/TheProject/src".toPath()) + } + + private fun assertBaseUri(sarif: String, expectedPath: Path) { + val sarifSchema210: SarifSchema210 = Json.decodeFromString(sarif) + + val run = sarifSchema210.runs.first() + + val result = run + .results + ?.first()!! + + val artifactLocation = result.fixes!!.first() + .artifactChanges + .first() + .artifactLocation + + assertEquals( + expectedPath, + resolveUriBaseId( + artifactLocation.getUriBaseIdForArtifactLocation(result), + run + ) + ) + } +} diff --git a/fixpatches/src/commonTest/resources/needsfix/NeedsFix.cs b/fixpatches/src/commonTest/resources/needsfix/NeedsFix.cs new file mode 100644 index 0000000..8d264e6 --- /dev/null +++ b/fixpatches/src/commonTest/resources/needsfix/NeedsFix.cs @@ -0,0 +1,9 @@ +using System; + +public class NeedsFix +{ + public NeedsFix() + { + // This woord is spelled wrong. + } +} \ No newline at end of file diff --git a/fixpatches/src/commonTest/resources/sarif-fixes-2.sarif b/fixpatches/src/commonTest/resources/sarif-fixes-2.sarif new file mode 100644 index 0000000..88d1674 --- /dev/null +++ b/fixpatches/src/commonTest/resources/sarif-fixes-2.sarif @@ -0,0 +1,126 @@ +{ + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [] + } + ], + "results": [ + { + "fixes": [ + { + "artifactChanges": [ + { + "artifactLocation": { + "uri": "targets/autofix/autofix.py" + }, + "replacements": [ + { + "deletedRegion": { + "endColumn": 16, + "endLine": 5, + "startColumn": 3, + "startLine": 5 + }, + "insertedContent": { + "text": " inputs.get(x) = 1" + } + } + ] + } + ], + "description": { + "text": "Use `.get()` method to avoid a KeyNotFound error\n Autofix: Semgrep rule suggested fix" + } + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "targets/autofix/autofix.py", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "endColumn": 16, + "endLine": 5, + "snippet": { + "text": " inputs[x] = 1" + }, + "startColumn": 3, + "startLine": 5 + } + } + } + ], + "message": { + "text": "Use `.get()` method to avoid a KeyNotFound error" + }, + "ruleId": "rules.autofix.use-dict-get" + }, + { + "fixes": [ + { + "artifactChanges": [ + { + "artifactLocation": { + "uri": "targets/autofix/autofix.py" + }, + "replacements": [ + { + "deletedRegion": { + "endColumn": 28, + "endLine": 6, + "startColumn": 3, + "startLine": 6 + }, + "insertedContent": { + "text": " if inputs.get(x + 1) == True:" + } + } + ] + } + ], + "description": { + "text": "Use `.get()` method to avoid a KeyNotFound error\n Autofix: Semgrep rule suggested fix" + } + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "targets/autofix/autofix.py", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "endColumn": 28, + "endLine": 6, + "snippet": { + "text": " if inputs[x + 1] == True:" + }, + "startColumn": 3, + "startLine": 6 + } + } + } + ], + "message": { + "text": "Use `.get()` method to avoid a KeyNotFound error" + }, + "ruleId": "rules.autofix.use-dict-get" + } + ], + "tool": { + "driver": { + "name": "semgrep", + "version": "placeholder" + } + } + } + ], + "version": "2.1.0" +} \ No newline at end of file diff --git a/fixpatches/src/commonTest/resources/sarif-fixes-3.sarif b/fixpatches/src/commonTest/resources/sarif-fixes-3.sarif new file mode 100644 index 0000000..1869e38 --- /dev/null +++ b/fixpatches/src/commonTest/resources/sarif-fixes-3.sarif @@ -0,0 +1,116 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "originalUriBaseIds": { + "%SRCROOT%": { + "uri": "." + } + }, + "results": [ + { + "fixes": [ + { + "artifactChanges": [ + { + "artifactLocation": { + "uri": "src/kotlin/Test1.kt" + }, + "replacements": [ + { + "deletedRegion": { + "endColumn": 19, + "endLine": 9, + "startColumn": 5, + "startLine": 9 + }, + "insertedContent": { + "text": "nameMyaSayR" + } + } + ] + }, + { + "artifactLocation": { + "uri": "src/kotlin/Test2.kt" + }, + "replacements": [ + { + "deletedRegion": { + "endColumn": 19, + "endLine": 9, + "startColumn": 5, + "startLine": 9 + }, + "insertedContent": { + "text": "nameMyaSayR" + } + } + ] + } + ], + "description": { + "text": "[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_" + } + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/kotlin/Test1.kt", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "endColumn": 19, + "endLine": 9, + "snippet": { + "text": "NAme_MYa_sayR_" + }, + "startColumn": 5, + "startLine": 9 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/kotlin/Test2.kt", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "endColumn": 19, + "endLine": 9, + "snippet": { + "text": "NAme_MYa_sayR_" + }, + "startColumn": 5, + "startLine": 9 + } + } + } + ], + "message": { + "text": "[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_" + }, + "ruleId": "diktat-ruleset:identifier-naming" + } + ], + "tool": { + "driver": { + "downloadUri": "https://github.com/pinterest/ktlint/releases/tag/0.42.0", + "fullName": "ktlint", + "informationUri": "https://github.com/pinterest/ktlint/", + "language": "en", + "name": "ktlint", + "organization": "pinterest", + "rules": [ + ], + "semanticVersion": "0.42.0", + "version": "0.42.0" + } + } + } + ] +} diff --git a/fixpatches/src/commonTest/resources/sarif-fixes.sarif b/fixpatches/src/commonTest/resources/sarif-fixes.sarif new file mode 100644 index 0000000..ee8cb9f --- /dev/null +++ b/fixpatches/src/commonTest/resources/sarif-fixes.sarif @@ -0,0 +1,81 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "originalUriBaseIds": { + "%SRCROOT%": { + "uri": "." + } + }, + "results": [ + { + "fixes": [ + { + "artifactChanges": [ + { + "artifactLocation": { + "uri": "src/kotlin/EnumValueSnakeCaseTest.kt" + }, + "replacements": [ + { + "deletedRegion": { + "endColumn": 19, + "endLine": 9, + "startColumn": 5, + "startLine": 9 + }, + "insertedContent": { + "text": "nameMyaSayR" + } + } + ] + } + ], + "description": { + "text": "[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_" + } + } + ], + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/kotlin/EnumValueSnakeCaseTest.kt", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "endColumn": 19, + "endLine": 9, + "snippet": { + "text": "NAme_MYa_sayR_" + }, + "startColumn": 5, + "startLine": 9 + } + } + } + ], + "message": { + "text": "[ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_" + }, + "ruleId": "diktat-ruleset:identifier-naming" + } + ], + "tool": { + "driver": { + "downloadUri": "https://github.com/pinterest/ktlint/releases/tag/0.42.0", + "fullName": "ktlint", + "informationUri": "https://github.com/pinterest/ktlint/", + "language": "en", + "name": "ktlint", + "organization": "pinterest", + "rules": [ + ], + "semanticVersion": "0.42.0", + "version": "0.42.0" + } + } + } + ] +} diff --git a/fixpatches/src/commonTest/resources/sarif-warn-and-fixes.sarif b/fixpatches/src/commonTest/resources/sarif-warn-and-fixes.sarif new file mode 100644 index 0000000..42d38f8 --- /dev/null +++ b/fixpatches/src/commonTest/resources/sarif-warn-and-fixes.sarif @@ -0,0 +1,104 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "SpellChecker", + "version": "1.0.0" + } + }, + "originalUriBaseIds": { + "SRCROOT": { + "uri": "." + } + }, + "results": [ + { + "ruleId": "SPELL1001", + "message": { + "text": "The word 'woord' is misspelled." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "Example.cs", + "uriBaseId": "SRCROOT", + "index": 0 + }, + "region": { + "startLine": 7, + "startColumn": 17, + "endColumn": 22 + } + } + } + ], + "fixes": [ + { + "description": { + "text": "Correct the spelling." + }, + "artifactChanges": [ + { + "artifactLocation": { + "uri": "needsfix/NeedsFix.cs", + "uriBaseId": "SRCROOT" + }, + "replacements": [ + { + "deletedRegion": { + "startLine": 7, + "startColumn": 17, + "endColumn": 22 + }, + "insertedContent": { + "text": "word" + } + } + ] + } + ] + }, + { + "description": { + "text": "Remove the word." + }, + "artifactChanges": [ + { + "artifactLocation": { + "uri": "needsfix/NeedsFix.cs", + "uriBaseId": "SRCROOT" + }, + "replacements": [ + { + "deletedRegion": { + "startLine": 7, + "startColumn": 17, + "endColumn": 23 + } + } + ] + } + ] + } + ] + } + ], + "artifacts": [ + { + "location": { + "uri": "Example.cs", + "uriBaseId": "SRCROOT" + }, + "contents": { + "text": "using System;\n\npublic class NeedsFix\n{\n public NeedsFix()\n {\n // This woord is spelled wrong.\n }\n}" + } + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/fixpatches/src/commonTest/resources/src/kotlin/EnumValueSnakeCaseTest.kt b/fixpatches/src/commonTest/resources/src/kotlin/EnumValueSnakeCaseTest.kt new file mode 100644 index 0000000..6fcf74c --- /dev/null +++ b/fixpatches/src/commonTest/resources/src/kotlin/EnumValueSnakeCaseTest.kt @@ -0,0 +1,10 @@ +// ;warn:2:9: [PACKAGE_NAME_INCORRECT_PREFIX] package name should start from company's domain: com.saveourtool.sarifutils{{.*}} +package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ + +// ;warn:4:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: EnumValueSnakeCaseTest (cannot be auto-corrected){{.*}} +// ;warn:9:5: [ENUMS_SEPARATED] enum is incorrectly formatted: enums must end with semicolon{{.*}} +// ;warn:9:5: [ENUMS_SEPARATED] enum is incorrectly formatted: last enum entry must end with a comma{{.*}} +enum class EnumValueSnakeCaseTest { + // ;warn:9:5: [ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_{{.*}} + NAme_MYa_sayR_ +} diff --git a/fixpatches/src/commonTest/resources/src/kotlin/Test1.kt b/fixpatches/src/commonTest/resources/src/kotlin/Test1.kt new file mode 100644 index 0000000..6fcf74c --- /dev/null +++ b/fixpatches/src/commonTest/resources/src/kotlin/Test1.kt @@ -0,0 +1,10 @@ +// ;warn:2:9: [PACKAGE_NAME_INCORRECT_PREFIX] package name should start from company's domain: com.saveourtool.sarifutils{{.*}} +package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ + +// ;warn:4:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: EnumValueSnakeCaseTest (cannot be auto-corrected){{.*}} +// ;warn:9:5: [ENUMS_SEPARATED] enum is incorrectly formatted: enums must end with semicolon{{.*}} +// ;warn:9:5: [ENUMS_SEPARATED] enum is incorrectly formatted: last enum entry must end with a comma{{.*}} +enum class EnumValueSnakeCaseTest { + // ;warn:9:5: [ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_{{.*}} + NAme_MYa_sayR_ +} diff --git a/fixpatches/src/commonTest/resources/src/kotlin/Test2.kt b/fixpatches/src/commonTest/resources/src/kotlin/Test2.kt new file mode 100644 index 0000000..6fcf74c --- /dev/null +++ b/fixpatches/src/commonTest/resources/src/kotlin/Test2.kt @@ -0,0 +1,10 @@ +// ;warn:2:9: [PACKAGE_NAME_INCORRECT_PREFIX] package name should start from company's domain: com.saveourtool.sarifutils{{.*}} +package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ + +// ;warn:4:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: EnumValueSnakeCaseTest (cannot be auto-corrected){{.*}} +// ;warn:9:5: [ENUMS_SEPARATED] enum is incorrectly formatted: enums must end with semicolon{{.*}} +// ;warn:9:5: [ENUMS_SEPARATED] enum is incorrectly formatted: last enum entry must end with a comma{{.*}} +enum class EnumValueSnakeCaseTest { + // ;warn:9:5: [ENUM_VALUE] enum values should be in selected UPPER_CASE snake/PascalCase format: NAme_MYa_sayR_{{.*}} + NAme_MYa_sayR_ +} diff --git a/fixpatches/src/commonTest/resources/targets/autofix/autofix.py b/fixpatches/src/commonTest/resources/targets/autofix/autofix.py new file mode 100644 index 0000000..2b88240 --- /dev/null +++ b/fixpatches/src/commonTest/resources/targets/autofix/autofix.py @@ -0,0 +1,7 @@ +import sys + +inputs = {} +for x in range(5): + inputs[x] = 1 + if inputs[x + 1] == True: + exit(1) diff --git a/fixpatches/src/jvmMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt b/fixpatches/src/jvmMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt new file mode 100644 index 0000000..2db9910 --- /dev/null +++ b/fixpatches/src/jvmMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt @@ -0,0 +1,11 @@ +/** + * Utility methods to work with file system + */ + +@file:JvmName("FileUtilsJvm") + +package com.saveourtool.sarifutils.cli.files + +import okio.FileSystem + +actual val fs: FileSystem = FileSystem.SYSTEM diff --git a/fixpatches/src/nativeMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt b/fixpatches/src/nativeMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt new file mode 100644 index 0000000..1058cb9 --- /dev/null +++ b/fixpatches/src/nativeMain/kotlin/com/saveourtool/sarifutils/cli/files/FileUtils.kt @@ -0,0 +1,9 @@ +/** + * Utility methods to work with file system + */ + +package com.saveourtool.sarifutils.cli.files + +import okio.FileSystem + +actual val fs: FileSystem = FileSystem.SYSTEM