Skip to content

Commit 80dcff4

Browse files
mmvpmVassiliy-Kudryashov
authored andcommitted
Support startColumn field in the SARIF report (#454)
1 parent b36a2b1 commit 80dcff4

File tree

8 files changed

+109
-32
lines changed

8 files changed

+109
-32
lines changed

utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import java.nio.file.Paths
77
object PathUtil {
88

99
/**
10-
* Creates a Path from the String
10+
* Creates a Path from the String.
1111
*/
1212
fun String.toPath(): Path = Paths.get(this)
1313

1414
/**
15-
* Finds a path for the [other] relative to the [root] if it is possible
15+
* Finds a path for the [other] relative to the [root] if it is possible.
1616
*
17-
* Example: safeRelativize("C:/project/", "C:/project/src/Main.java") = "src/Main.java"
17+
* Example: safeRelativize("C:/project/", "C:/project/src/Main.java") = "src/Main.java".
1818
*/
1919
fun safeRelativize(root: String?, other: String?): String? {
2020
if (root == null || other == null)
@@ -28,9 +28,9 @@ object PathUtil {
2828
}
2929

3030
/**
31-
* Removes class fully qualified name from absolute path to the file if it is possible
31+
* Removes class fully qualified name from absolute path to the file if it is possible.
3232
*
33-
* Example: removeClassFqnFromPath("C:/project/src/com/Main.java", "com.Main") = "C:/project/src/"
33+
* Example: removeClassFqnFromPath("C:/project/src/com/Main.java", "com.Main") = "C:/project/src/".
3434
*/
3535
fun removeClassFqnFromPath(sourceAbsolutePath: String?, classFqn: String?): String? {
3636
if (sourceAbsolutePath == null || classFqn == null)
@@ -48,9 +48,9 @@ object PathUtil {
4848
}
4949

5050
/**
51-
* Resolves `pathToResolve` against `absolutePath` and checks if a resolved path exists
51+
* Resolves [toResolve] against [absolute] and checks if a resolved path exists.
5252
*
53-
* Example: resolveIfExists("C:/project/src/", "Main.java") = "C:/project/src/Main.java"
53+
* Example: resolveIfExists("C:/project/src/", "Main.java") = "C:/project/src/Main.java".
5454
*/
5555
fun resolveIfExists(absolute: String, toResolve: String): String? {
5656
val absolutePath = absolute.toPath()
@@ -64,19 +64,19 @@ object PathUtil {
6464
}
6565

6666
/**
67-
* Replaces '\\' in the [path] with '/'
67+
* Replaces '\\' in the [path] with '/'.
6868
*/
6969
fun replaceSeparator(path: String): String =
7070
path.replace('\\', '/')
7171

7272
/**
73-
* Replaces '.' in the [classFqn] with '/'
73+
* Replaces '.' in the [classFqn] with '/'.
7474
*/
7575
fun classFqnToPath(classFqn: String): String =
7676
classFqn.replace('.', '/')
7777

7878
/**
79-
* Returns a URL to represent this path
79+
* Returns a URL to represent this path.
8080
*/
8181
fun Path.toURL(): URL =
8282
this.toUri().toURL()
@@ -88,7 +88,7 @@ object PathUtil {
8888
"""<a href="file:///$filePath">${fileName}</a>"""
8989

9090
/**
91-
* Returns the extension of this file (including the dot)
91+
* Returns the extension of this file (including the dot).
9292
*/
9393
val Path.fileExtension: String
9494
get() = "." + this.toFile().extension

utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,27 @@ data class SarifArtifact(
142142
val uriBaseId: String = "%SRCROOT%"
143143
)
144144

145+
// all fields should be one-based
145146
data class SarifRegion(
146147
val startLine: Int,
147148
val endLine: Int? = null,
148149
val startColumn: Int? = null,
149150
val endColumn: Int? = null
150-
)
151+
) {
152+
companion object {
153+
/**
154+
* Makes [startColumn] the first non-whitespace character in [startLine] in the [text].
155+
* If the [text] contains less than [startLine] lines, [startColumn] == null.
156+
*/
157+
fun withStartLine(text: String, startLine: Int): SarifRegion {
158+
val neededLine = text.split('\n').getOrNull(startLine - 1) // to zero-based
159+
val startColumn = neededLine?.let {
160+
neededLine.takeWhile { it.toString().isBlank() }.length + 1 // to one-based
161+
}
162+
return SarifRegion(startLine = startLine, startColumn = startColumn)
163+
}
164+
}
165+
}
151166

152167
// related locations
153168

utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ class SarifReport(
144144
if (classFqn == null)
145145
return listOf()
146146
val sourceRelativePath = sourceFinding.getSourceRelativePath(classFqn)
147-
val sourceRegion = SarifRegion(
148-
startLine = extractLineNumber(utExecution) ?: defaultLineNumber
149-
)
147+
val startLine = extractLineNumber(utExecution) ?: defaultLineNumber
148+
val sourceCode = sourceFinding.getSourceFile(classFqn)?.readText() ?: ""
149+
val sourceRegion = SarifRegion.withStartLine(sourceCode, startLine)
150150
return listOf(
151151
SarifPhysicalLocationWrapper(
152152
SarifPhysicalLocation(SarifArtifact(sourceRelativePath), sourceRegion)
@@ -155,14 +155,13 @@ class SarifReport(
155155
}
156156

157157
private fun getRelatedLocations(utExecution: UtExecution): List<SarifRelatedPhysicalLocationWrapper> {
158-
val lineNumber = generatedTestsCode.split('\n').indexOfFirst { line ->
159-
utExecution.testMethodName?.let { testMethodName ->
160-
line.contains(testMethodName)
161-
} ?: false
162-
}
163-
val sourceRegion = SarifRegion(
164-
startLine = if (lineNumber != -1) lineNumber + 1 else defaultLineNumber
165-
)
158+
val startLine = utExecution.testMethodName?.let { testMethodName ->
159+
val neededLine = generatedTestsCode.split('\n').indexOfFirst { line ->
160+
line.contains("$testMethodName(")
161+
}
162+
if (neededLine == -1) null else neededLine + 1 // to one-based
163+
} ?: defaultLineNumber
164+
val sourceRegion = SarifRegion.withStartLine(generatedTestsCode, startLine)
166165
return listOf(
167166
SarifRelatedPhysicalLocationWrapper(
168167
relatedLocationId,
@@ -228,14 +227,15 @@ class SarifReport(
228227
return null
229228
val extension = stackTraceElement.fileName?.toPath()?.fileExtension
230229
val relativePath = sourceFinding.getSourceRelativePath(stackTraceElement.className, extension)
230+
val sourceCode = sourceFinding.getSourceFile(stackTraceElement.className, extension)?.readText() ?: ""
231231
return SarifFlowLocationWrapper(
232232
SarifFlowLocation(
233233
message = Message(
234234
text = stackTraceElement.toString()
235235
),
236236
physicalLocation = SarifPhysicalLocation(
237237
SarifArtifact(relativePath),
238-
SarifRegion(lineNumber)
238+
SarifRegion.withStartLine(sourceCode, lineNumber)
239239
)
240240
)
241241
)
@@ -252,21 +252,36 @@ class SarifReport(
252252
}
253253
if (testMethodStartsAt == -1)
254254
return null
255+
/**
256+
* ...
257+
* public void testMethodName() { // <- `testMethodStartsAt`
258+
* ...
259+
* className.methodName(...) // <- needed `startLine`
260+
* ...
261+
* }
262+
*/
255263

256264
// searching needed method call
257265
val publicMethodCallPattern = "$methodName("
258266
val privateMethodCallPattern = Regex("""$methodName.*\.invoke\(""") // using reflection
259-
val methodCallLineNumber = testsBodyLines
267+
val methodCallShiftInTestMethod = testsBodyLines
260268
.drop(testMethodStartsAt + 1) // for search after it
261269
.indexOfFirst { line ->
262270
line.contains(publicMethodCallPattern) || line.contains(privateMethodCallPattern)
263271
}
264-
if (methodCallLineNumber == -1)
272+
if (methodCallShiftInTestMethod == -1)
265273
return null
266274

275+
// `startLine` consists of:
276+
// shift to the testMethod call (+ testMethodStartsAt)
277+
// the line with testMethodName (+ 1)
278+
// shift to the method call (+ methodCallShiftInTestMethod)
279+
// to one-based (+ 1)
280+
val startLine = testMethodStartsAt + 1 + methodCallShiftInTestMethod + 1
281+
267282
return SarifPhysicalLocation(
268283
SarifArtifact(sourceFinding.testsRelativePath),
269-
SarifRegion(startLine = methodCallLineNumber + 1 + testMethodStartsAt + 1)
284+
SarifRegion.withStartLine(generatedTestsCode, startLine)
270285
)
271286
}
272287

utbot-framework/src/main/kotlin/org/utbot/sarif/SourceFindingStrategy.kt

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.utbot.common.PathUtil
44
import org.utbot.common.PathUtil.classFqnToPath
55
import org.utbot.common.PathUtil.fileExtension
66
import org.utbot.common.PathUtil.toPath
7+
import java.io.File
78

89
/**
910
* Defines the search strategy for the source files. Used when creating a SARIF report.
@@ -20,6 +21,11 @@ abstract class SourceFindingStrategy {
2021
* Returns a path to the source file by given [classFqn].
2122
*/
2223
abstract fun getSourceRelativePath(classFqn: String, extension: String? = null): String
24+
25+
/**
26+
* Returns the source file by given [classFqn].
27+
*/
28+
abstract fun getSourceFile(classFqn: String, extension: String? = null): File?
2329
}
2430

2531
/**
@@ -41,13 +47,13 @@ class SourceFindingStrategyDefault(
4147
) : SourceFindingStrategy() {
4248

4349
/**
44-
* Tries to construct the relative path to tests (against `projectRootPath`) using the `testsFilePath`
50+
* Tries to construct the relative path to tests (against `projectRootPath`) using the `testsFilePath`.
4551
*/
4652
override val testsRelativePath =
4753
PathUtil.safeRelativize(projectRootPath, testsFilePath) ?: testsFilePath.toPath().fileName.toString()
4854

4955
/**
50-
* Tries to guess the relative path (against `projectRootPath`) to the source file containing the class [classFqn]
56+
* Tries to guess the relative path (against `projectRootPath`) to the source file containing the class [classFqn].
5157
*/
5258
override fun getSourceRelativePath(classFqn: String, extension: String?): String {
5359
val fileExtension = extension ?: sourceExtension
@@ -56,6 +62,16 @@ class SourceFindingStrategyDefault(
5662
return relativePath ?: (classFqnToPath(classFqn) + fileExtension)
5763
}
5864

65+
/**
66+
* Tries to find the source file containing the class [classFqn].
67+
* Returns null if the file does not exist.
68+
*/
69+
override fun getSourceFile(classFqn: String, extension: String?): File? {
70+
val fileExtension = extension ?: sourceExtension
71+
val absolutePath = resolveClassFqn(sourceFilesDirectory, classFqn, fileExtension)
72+
return absolutePath?.let(::File)
73+
}
74+
5975
// internal
6076

6177
private val sourceExtension = sourceFilePath.toPath().fileExtension
@@ -64,8 +80,9 @@ class SourceFindingStrategyDefault(
6480
PathUtil.removeClassFqnFromPath(sourceFilePath, sourceClassFqn)
6581

6682
/**
67-
* Resolves [classFqn] against [absolutePath] and checks if a resolved path exists
68-
* Example: resolveClassFqn("C:/project/src/", "com.Main") = "C:/project/src/com/Main.java"
83+
* Resolves [classFqn] against [absolutePath] and checks if a resolved path exists.
84+
*
85+
* Example: resolveClassFqn("C:/project/src/", "com.Main") = "C:/project/src/com/Main.java".
6986
*/
7087
private fun resolveClassFqn(absolutePath: String?, classFqn: String, extension: String = ".java"): String? {
7188
if (absolutePath == null)

utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class SarifReportTest {
8686
assert(location.region.startLine == 1337)
8787
assert(relatedLocation.artifactLocation.uri.contains("MainTest.java"))
8888
assert(relatedLocation.region.startLine == 1)
89+
assert(relatedLocation.region.startColumn == 1)
8990
}
9091

9192
@Test
@@ -158,6 +159,7 @@ class SarifReportTest {
158159
}
159160
assert(codeFlowPhysicalLocations[0].artifactLocation.uri.contains("MainTest.java"))
160161
assert(codeFlowPhysicalLocations[0].region.startLine == 3)
162+
assert(codeFlowPhysicalLocations[0].region.startColumn == 7)
161163
}
162164

163165
@Test
@@ -181,6 +183,7 @@ class SarifReportTest {
181183
}
182184
assert(codeFlowPhysicalLocations[0].artifactLocation.uri.contains("MainTest.java"))
183185
assert(codeFlowPhysicalLocations[0].region.startLine == 4)
186+
assert(codeFlowPhysicalLocations[0].region.startColumn == 5)
184187
}
185188

186189
// internal
@@ -217,7 +220,7 @@ class SarifReportTest {
217220
private val generatedTestsCodeMain = """
218221
public void testMain_ThrowArithmeticException() {
219222
Main main = new Main();
220-
main.main(0);
223+
main.main(0); // shift for `startColumn` == 7
221224
}
222225
""".trimIndent()
223226

utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.utbot.gradle.plugin.wrappers
33
import org.utbot.common.PathUtil
44
import org.utbot.common.PathUtil.toPath
55
import org.utbot.sarif.SourceFindingStrategy
6+
import java.io.File
67

78
/**
89
* The search strategy based on the information available to the Gradle.
@@ -37,6 +38,13 @@ class SourceFindingStrategyGradle(
3738
} ?: defaultPath
3839
}
3940

41+
/**
42+
* Finds the source file containing the class [classFqn].
43+
* Returns null if the file does not exist.
44+
*/
45+
override fun getSourceFile(classFqn: String, extension: String?): File? =
46+
sourceSet.findSourceCodeFile(classFqn)
47+
4048
// internal
4149

4250
private val projectRootPath = sourceSet.parentProject.sarifProperties.projectRoot.absolutePath

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.utbot.sarif.SourceFindingStrategy
77
import com.intellij.psi.JavaPsiFacade
88
import com.intellij.psi.PsiClass
99
import org.jetbrains.kotlin.idea.search.allScope
10+
import java.io.File
1011

1112
/**
1213
* The search strategy based on the information available to the PsiClass
@@ -29,6 +30,16 @@ class SourceFindingStrategyIdea(testClass: PsiClass) : SourceFindingStrategy() {
2930
safeRelativize(project.basePath, psiClass.containingFile.virtualFile.path)
3031
} ?: (classFqnToPath(classFqn) + (extension ?: defaultExtension))
3132

33+
/**
34+
* Finds the source file containing the class [classFqn].
35+
* Returns null if the file does not exist.
36+
*/
37+
override fun getSourceFile(classFqn: String, extension: String?): File? {
38+
val psiClass = JavaPsiFacade.getInstance(project).findClass(classFqn, project.allScope())
39+
val sourceCodeFile = psiClass?.containingFile?.virtualFile?.path?.let(::File)
40+
return if (sourceCodeFile?.exists() == true) sourceCodeFile else null
41+
}
42+
3243
// internal
3344

3445
private val project = testClass.project

utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.utbot.maven.plugin.wrappers
33
import org.utbot.common.PathUtil
44
import org.utbot.common.PathUtil.toPath
55
import org.utbot.sarif.SourceFindingStrategy
6+
import java.io.File
67

78
/**
89
* The search strategy based on the information available to the Maven.
@@ -37,6 +38,13 @@ class SourceFindingStrategyMaven(
3738
} ?: defaultPath
3839
}
3940

41+
/**
42+
* Finds the source file containing the class [classFqn].
43+
* Returns null if the file does not exist.
44+
*/
45+
override fun getSourceFile(classFqn: String, extension: String?): File? =
46+
mavenProjectWrapper.findSourceCodeFile(classFqn)
47+
4048
// internal
4149

4250
private val projectRootPath = mavenProjectWrapper.sarifProperties.projectRoot.absolutePath

0 commit comments

Comments
 (0)