diff --git a/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt index 3f5f0070e8..cad7603e2a 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt @@ -7,14 +7,14 @@ import java.nio.file.Paths object PathUtil { /** - * Creates a Path from the String + * Creates a Path from the String. */ fun String.toPath(): Path = Paths.get(this) /** - * Finds a path for the [other] relative to the [root] if it is possible + * Finds a path for the [other] relative to the [root] if it is possible. * - * Example: safeRelativize("C:/project/", "C:/project/src/Main.java") = "src/Main.java" + * Example: safeRelativize("C:/project/", "C:/project/src/Main.java") = "src/Main.java". */ fun safeRelativize(root: String?, other: String?): String? { if (root == null || other == null) @@ -28,9 +28,9 @@ object PathUtil { } /** - * Removes class fully qualified name from absolute path to the file if it is possible + * Removes class fully qualified name from absolute path to the file if it is possible. * - * Example: removeClassFqnFromPath("C:/project/src/com/Main.java", "com.Main") = "C:/project/src/" + * Example: removeClassFqnFromPath("C:/project/src/com/Main.java", "com.Main") = "C:/project/src/". */ fun removeClassFqnFromPath(sourceAbsolutePath: String?, classFqn: String?): String? { if (sourceAbsolutePath == null || classFqn == null) @@ -48,9 +48,9 @@ object PathUtil { } /** - * Resolves `pathToResolve` against `absolutePath` and checks if a resolved path exists + * Resolves [toResolve] against [absolute] and checks if a resolved path exists. * - * Example: resolveIfExists("C:/project/src/", "Main.java") = "C:/project/src/Main.java" + * Example: resolveIfExists("C:/project/src/", "Main.java") = "C:/project/src/Main.java". */ fun resolveIfExists(absolute: String, toResolve: String): String? { val absolutePath = absolute.toPath() @@ -64,19 +64,19 @@ object PathUtil { } /** - * Replaces '\\' in the [path] with '/' + * Replaces '\\' in the [path] with '/'. */ fun replaceSeparator(path: String): String = path.replace('\\', '/') /** - * Replaces '.' in the [classFqn] with '/' + * Replaces '.' in the [classFqn] with '/'. */ fun classFqnToPath(classFqn: String): String = classFqn.replace('.', '/') /** - * Returns a URL to represent this path + * Returns a URL to represent this path. */ fun Path.toURL(): URL = this.toUri().toURL() @@ -88,7 +88,7 @@ object PathUtil { """${fileName}""" /** - * Returns the extension of this file (including the dot) + * Returns the extension of this file (including the dot). */ val Path.fileExtension: String get() = "." + this.toFile().extension diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt index 1109f46d89..1a6aae7e8e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt @@ -142,12 +142,27 @@ data class SarifArtifact( val uriBaseId: String = "%SRCROOT%" ) +// all fields should be one-based data class SarifRegion( val startLine: Int, val endLine: Int? = null, val startColumn: Int? = null, val endColumn: Int? = null -) +) { + companion object { + /** + * Makes [startColumn] the first non-whitespace character in [startLine] in the [text]. + * If the [text] contains less than [startLine] lines, [startColumn] == null. + */ + fun withStartLine(text: String, startLine: Int): SarifRegion { + val neededLine = text.split('\n').getOrNull(startLine - 1) // to zero-based + val startColumn = neededLine?.let { + neededLine.takeWhile { it.toString().isBlank() }.length + 1 // to one-based + } + return SarifRegion(startLine = startLine, startColumn = startColumn) + } + } +} // related locations diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt index 286476b075..de414528a4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt @@ -144,9 +144,9 @@ class SarifReport( if (classFqn == null) return listOf() val sourceRelativePath = sourceFinding.getSourceRelativePath(classFqn) - val sourceRegion = SarifRegion( - startLine = extractLineNumber(utExecution) ?: defaultLineNumber - ) + val startLine = extractLineNumber(utExecution) ?: defaultLineNumber + val sourceCode = sourceFinding.getSourceFile(classFqn)?.readText() ?: "" + val sourceRegion = SarifRegion.withStartLine(sourceCode, startLine) return listOf( SarifPhysicalLocationWrapper( SarifPhysicalLocation(SarifArtifact(sourceRelativePath), sourceRegion) @@ -155,14 +155,13 @@ class SarifReport( } private fun getRelatedLocations(utExecution: UtExecution): List { - val lineNumber = generatedTestsCode.split('\n').indexOfFirst { line -> - utExecution.testMethodName?.let { testMethodName -> - line.contains(testMethodName) - } ?: false - } - val sourceRegion = SarifRegion( - startLine = if (lineNumber != -1) lineNumber + 1 else defaultLineNumber - ) + val startLine = utExecution.testMethodName?.let { testMethodName -> + val neededLine = generatedTestsCode.split('\n').indexOfFirst { line -> + line.contains("$testMethodName(") + } + if (neededLine == -1) null else neededLine + 1 // to one-based + } ?: defaultLineNumber + val sourceRegion = SarifRegion.withStartLine(generatedTestsCode, startLine) return listOf( SarifRelatedPhysicalLocationWrapper( relatedLocationId, @@ -228,6 +227,7 @@ class SarifReport( return null val extension = stackTraceElement.fileName?.toPath()?.fileExtension val relativePath = sourceFinding.getSourceRelativePath(stackTraceElement.className, extension) + val sourceCode = sourceFinding.getSourceFile(stackTraceElement.className, extension)?.readText() ?: "" return SarifFlowLocationWrapper( SarifFlowLocation( message = Message( @@ -235,7 +235,7 @@ class SarifReport( ), physicalLocation = SarifPhysicalLocation( SarifArtifact(relativePath), - SarifRegion(lineNumber) + SarifRegion.withStartLine(sourceCode, lineNumber) ) ) ) @@ -252,21 +252,36 @@ class SarifReport( } if (testMethodStartsAt == -1) return null + /** + * ... + * public void testMethodName() { // <- `testMethodStartsAt` + * ... + * className.methodName(...) // <- needed `startLine` + * ... + * } + */ // searching needed method call val publicMethodCallPattern = "$methodName(" val privateMethodCallPattern = Regex("""$methodName.*\.invoke\(""") // using reflection - val methodCallLineNumber = testsBodyLines + val methodCallShiftInTestMethod = testsBodyLines .drop(testMethodStartsAt + 1) // for search after it .indexOfFirst { line -> line.contains(publicMethodCallPattern) || line.contains(privateMethodCallPattern) } - if (methodCallLineNumber == -1) + if (methodCallShiftInTestMethod == -1) return null + // `startLine` consists of: + // shift to the testMethod call (+ testMethodStartsAt) + // the line with testMethodName (+ 1) + // shift to the method call (+ methodCallShiftInTestMethod) + // to one-based (+ 1) + val startLine = testMethodStartsAt + 1 + methodCallShiftInTestMethod + 1 + return SarifPhysicalLocation( SarifArtifact(sourceFinding.testsRelativePath), - SarifRegion(startLine = methodCallLineNumber + 1 + testMethodStartsAt + 1) + SarifRegion.withStartLine(generatedTestsCode, startLine) ) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/SourceFindingStrategy.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/SourceFindingStrategy.kt index 88a87466ee..b2ef8b82df 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/SourceFindingStrategy.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/SourceFindingStrategy.kt @@ -4,6 +4,7 @@ import org.utbot.common.PathUtil import org.utbot.common.PathUtil.classFqnToPath import org.utbot.common.PathUtil.fileExtension import org.utbot.common.PathUtil.toPath +import java.io.File /** * Defines the search strategy for the source files. Used when creating a SARIF report. @@ -20,6 +21,11 @@ abstract class SourceFindingStrategy { * Returns a path to the source file by given [classFqn]. */ abstract fun getSourceRelativePath(classFqn: String, extension: String? = null): String + + /** + * Returns the source file by given [classFqn]. + */ + abstract fun getSourceFile(classFqn: String, extension: String? = null): File? } /** @@ -41,13 +47,13 @@ class SourceFindingStrategyDefault( ) : SourceFindingStrategy() { /** - * Tries to construct the relative path to tests (against `projectRootPath`) using the `testsFilePath` + * Tries to construct the relative path to tests (against `projectRootPath`) using the `testsFilePath`. */ override val testsRelativePath = PathUtil.safeRelativize(projectRootPath, testsFilePath) ?: testsFilePath.toPath().fileName.toString() /** - * Tries to guess the relative path (against `projectRootPath`) to the source file containing the class [classFqn] + * Tries to guess the relative path (against `projectRootPath`) to the source file containing the class [classFqn]. */ override fun getSourceRelativePath(classFqn: String, extension: String?): String { val fileExtension = extension ?: sourceExtension @@ -56,6 +62,16 @@ class SourceFindingStrategyDefault( return relativePath ?: (classFqnToPath(classFqn) + fileExtension) } + /** + * Tries to find the source file containing the class [classFqn]. + * Returns null if the file does not exist. + */ + override fun getSourceFile(classFqn: String, extension: String?): File? { + val fileExtension = extension ?: sourceExtension + val absolutePath = resolveClassFqn(sourceFilesDirectory, classFqn, fileExtension) + return absolutePath?.let(::File) + } + // internal private val sourceExtension = sourceFilePath.toPath().fileExtension @@ -64,8 +80,9 @@ class SourceFindingStrategyDefault( PathUtil.removeClassFqnFromPath(sourceFilePath, sourceClassFqn) /** - * Resolves [classFqn] against [absolutePath] and checks if a resolved path exists - * Example: resolveClassFqn("C:/project/src/", "com.Main") = "C:/project/src/com/Main.java" + * Resolves [classFqn] against [absolutePath] and checks if a resolved path exists. + * + * Example: resolveClassFqn("C:/project/src/", "com.Main") = "C:/project/src/com/Main.java". */ private fun resolveClassFqn(absolutePath: String?, classFqn: String, extension: String = ".java"): String? { if (absolutePath == null) diff --git a/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt b/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt index fad4ff0334..d3430d5218 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt @@ -86,6 +86,7 @@ class SarifReportTest { assert(location.region.startLine == 1337) assert(relatedLocation.artifactLocation.uri.contains("MainTest.java")) assert(relatedLocation.region.startLine == 1) + assert(relatedLocation.region.startColumn == 1) } @Test @@ -158,6 +159,7 @@ class SarifReportTest { } assert(codeFlowPhysicalLocations[0].artifactLocation.uri.contains("MainTest.java")) assert(codeFlowPhysicalLocations[0].region.startLine == 3) + assert(codeFlowPhysicalLocations[0].region.startColumn == 7) } @Test @@ -181,6 +183,7 @@ class SarifReportTest { } assert(codeFlowPhysicalLocations[0].artifactLocation.uri.contains("MainTest.java")) assert(codeFlowPhysicalLocations[0].region.startLine == 4) + assert(codeFlowPhysicalLocations[0].region.startColumn == 5) } // internal @@ -217,7 +220,7 @@ class SarifReportTest { private val generatedTestsCodeMain = """ public void testMain_ThrowArithmeticException() { Main main = new Main(); - main.main(0); + main.main(0); // shift for `startColumn` == 7 } """.trimIndent() diff --git a/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt index e45a190a17..7f7fc5f093 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt @@ -3,6 +3,7 @@ package org.utbot.gradle.plugin.wrappers import org.utbot.common.PathUtil import org.utbot.common.PathUtil.toPath import org.utbot.sarif.SourceFindingStrategy +import java.io.File /** * The search strategy based on the information available to the Gradle. @@ -37,6 +38,13 @@ class SourceFindingStrategyGradle( } ?: defaultPath } + /** + * Finds the source file containing the class [classFqn]. + * Returns null if the file does not exist. + */ + override fun getSourceFile(classFqn: String, extension: String?): File? = + sourceSet.findSourceCodeFile(classFqn) + // internal private val projectRootPath = sourceSet.parentProject.sarifProperties.projectRoot.absolutePath diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt index 0c92eed139..8aba1e3158 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt @@ -7,6 +7,7 @@ import org.utbot.sarif.SourceFindingStrategy import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import org.jetbrains.kotlin.idea.search.allScope +import java.io.File /** * The search strategy based on the information available to the PsiClass @@ -29,6 +30,16 @@ class SourceFindingStrategyIdea(testClass: PsiClass) : SourceFindingStrategy() { safeRelativize(project.basePath, psiClass.containingFile.virtualFile.path) } ?: (classFqnToPath(classFqn) + (extension ?: defaultExtension)) + /** + * Finds the source file containing the class [classFqn]. + * Returns null if the file does not exist. + */ + override fun getSourceFile(classFqn: String, extension: String?): File? { + val psiClass = JavaPsiFacade.getInstance(project).findClass(classFqn, project.allScope()) + val sourceCodeFile = psiClass?.containingFile?.virtualFile?.path?.let(::File) + return if (sourceCodeFile?.exists() == true) sourceCodeFile else null + } + // internal private val project = testClass.project diff --git a/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt index 515aad43be..4d6e1adf1c 100644 --- a/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt +++ b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt @@ -3,6 +3,7 @@ package org.utbot.maven.plugin.wrappers import org.utbot.common.PathUtil import org.utbot.common.PathUtil.toPath import org.utbot.sarif.SourceFindingStrategy +import java.io.File /** * The search strategy based on the information available to the Maven. @@ -37,6 +38,13 @@ class SourceFindingStrategyMaven( } ?: defaultPath } + /** + * Finds the source file containing the class [classFqn]. + * Returns null if the file does not exist. + */ + override fun getSourceFile(classFqn: String, extension: String?): File? = + mavenProjectWrapper.findSourceCodeFile(classFqn) + // internal private val projectRootPath = mavenProjectWrapper.sarifProperties.projectRoot.absolutePath