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