Skip to content

Commit a582daa

Browse files
committed
Add sarif logical locations
1 parent e4cf410 commit a582daa

File tree

4 files changed

+193
-64
lines changed

4 files changed

+193
-64
lines changed

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.utbot.sarif
22

3-
import com.fasterxml.jackson.annotation.JsonInclude
4-
import com.fasterxml.jackson.annotation.JsonProperty
5-
import com.fasterxml.jackson.annotation.JsonValue
3+
import com.fasterxml.jackson.annotation.*
4+
import com.fasterxml.jackson.core.JsonParser
5+
import com.fasterxml.jackson.databind.DeserializationContext
6+
import com.fasterxml.jackson.databind.JsonDeserializer
7+
import com.fasterxml.jackson.databind.JsonNode
8+
import com.fasterxml.jackson.databind.module.SimpleModule
69
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
710
import com.fasterxml.jackson.module.kotlin.readValue
811

@@ -17,6 +20,12 @@ data class Sarif(
1720
val runs: List<SarifRun>
1821
) {
1922
companion object {
23+
24+
private val jsonMapper = jacksonObjectMapper()
25+
.registerModule(SimpleModule()
26+
.addDeserializer(SarifLocationWrapper::class.java, SarifLocationWrapperDeserializer())
27+
)
28+
2029
private const val defaultSchema =
2130
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
2231
private const val defaultVersion =
@@ -29,11 +38,11 @@ data class Sarif(
2938
Sarif(defaultSchema, defaultVersion, listOf(run))
3039

3140
fun fromJson(reportInJson: String): Sarif =
32-
jacksonObjectMapper().readValue(reportInJson)
41+
jsonMapper.readValue(reportInJson)
3342
}
3443

3544
fun toJson(): String =
36-
jacksonObjectMapper()
45+
jsonMapper
3746
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
3847
.writerWithDefaultPrettyPrinter()
3948
.writeValueAsString(this)
@@ -111,7 +120,7 @@ data class SarifResult(
111120
val ruleId: String,
112121
val level: Level,
113122
val message: Message,
114-
val locations: List<SarifPhysicalLocationWrapper> = listOf(),
123+
val locations: List<SarifLocationWrapper> = listOf(),
115124
val relatedLocations: List<SarifRelatedPhysicalLocationWrapper> = listOf(),
116125
val codeFlows: List<SarifCodeFlow> = listOf()
117126
) {
@@ -145,14 +154,41 @@ data class Message(
145154
val markdown: String? = null
146155
)
147156

148-
// physical location
157+
// location
149158

150159
/**
151160
* [Documentation](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#location-object)
152161
*/
162+
sealed class SarifLocationWrapper
163+
153164
data class SarifPhysicalLocationWrapper(
154-
val physicalLocation: SarifPhysicalLocation,
155-
)
165+
val physicalLocation: SarifPhysicalLocation // this name used in the SarifLocationWrapperDeserializer
166+
) : SarifLocationWrapper()
167+
168+
data class SarifLogicalLocationsWrapper(
169+
val logicalLocations: List<SarifLogicalLocation> // this name used in the SarifLocationWrapperDeserializer
170+
) : SarifLocationWrapper()
171+
172+
/**
173+
* Custom JSON deserializer for sealed class [SarifLocationWrapper].
174+
* Returns [SarifPhysicalLocationWrapper] or [SarifLogicalLocationsWrapper].
175+
*/
176+
class SarifLocationWrapperDeserializer : JsonDeserializer<SarifLocationWrapper>() {
177+
override fun deserialize(jp: JsonParser, context: DeserializationContext?): SarifLocationWrapper {
178+
val node: JsonNode = jp.codec.readTree(jp)
179+
val isPhysicalLocation = node.get("physicalLocation") != null // field name
180+
val isLogicalLocations = node.get("logicalLocations") != null // field name
181+
return when {
182+
isPhysicalLocation -> {
183+
jacksonObjectMapper().readValue<SarifPhysicalLocationWrapper>(node.toString())
184+
}
185+
isLogicalLocations -> {
186+
return jacksonObjectMapper().readValue<SarifLogicalLocationsWrapper>(node.toString())
187+
}
188+
else -> error("SarifLocationWrapperDeserializer: Cannot parse ${node.toPrettyString()}")
189+
}
190+
}
191+
}
156192

157193
/**
158194
* [Documentation](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#physicallocation-object)
@@ -189,6 +225,15 @@ data class SarifRegion(
189225
}
190226
}
191227

228+
// logical locations
229+
230+
/**
231+
* [Documentation](https://github.com/microsoft/sarif-tutorials/blob/main/docs/2-Basics.md#physical-and-logical-locations)
232+
*/
233+
data class SarifLogicalLocation(
234+
val fullyQualifiedName: String
235+
)
236+
192237
// related locations
193238

194239
/**

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

Lines changed: 113 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.utbot.common.PathUtil.fileExtension
44
import org.utbot.common.PathUtil.toPath
55
import org.utbot.framework.UtSettings
66
import org.utbot.framework.plugin.api.*
7+
import java.nio.file.Path
78
import kotlin.io.path.nameWithoutExtension
89

910
/**
@@ -27,6 +28,41 @@ class SarifReport(
2728
reports.fold(Sarif.empty()) { sarif: Sarif, report: String ->
2829
sarif.copy(runs = sarif.runs + Sarif.fromJson(report).runs)
2930
}.toJson()
31+
32+
/**
33+
* Minimizes SARIF results between several reports.
34+
*
35+
* More complex version of the [SarifReport.minimizeResults].
36+
*/
37+
fun minimizeSarifResults(srcPathToSarif: MutableMap<Path, Sarif>): MutableMap<Path, Sarif> {
38+
val pathToSarifResult = srcPathToSarif.entries.flatMap { (path, sarif) ->
39+
sarif.getAllResults().map { sarifResult ->
40+
path to sarifResult
41+
}
42+
}
43+
val groupedResults = pathToSarifResult.groupBy { (_, sarifResult) ->
44+
sarifResult.ruleId to sarifResult.locations
45+
}
46+
val minimizedResults = groupedResults.map { (_, sarifResultsGroup) ->
47+
sarifResultsGroup.minByOrNull { (_, sarifResult) ->
48+
sarifResult.totalCodeFlowLocations()
49+
}!!
50+
}
51+
val groupedByPath = minimizedResults
52+
.groupBy { (path, _) -> path }
53+
.mapValues { (_, resultsWithPath) ->
54+
resultsWithPath.map { (_, sarifResult) -> sarifResult } // remove redundant path
55+
}
56+
val pathToSarifTool = srcPathToSarif.mapValues { (_, sarif) ->
57+
sarif.runs.first().tool
58+
}
59+
val paths = pathToSarifTool.keys intersect groupedByPath.keys
60+
return paths.associateWith { path ->
61+
val sarifTool = pathToSarifTool[path]!!
62+
val sarifResults = groupedByPath[path]!!
63+
Sarif.fromRun(SarifRun(sarifTool, sarifResults))
64+
}.toMutableMap()
65+
}
3066
}
3167

3268
/**
@@ -67,6 +103,8 @@ class SarifReport(
67103
*/
68104
private val relatedLocationId = 1 // for attaching link to generated test in related locations
69105

106+
private val stackTraceLengthForStackOverflow = 50 // stack overflow error may have too many elements
107+
70108
/**
71109
* Minimizes detected errors and removes duplicates.
72110
*
@@ -125,7 +163,7 @@ class SarifReport(
125163
[Generated test for this case]($relatedLocationId)
126164
""".trimIndent()
127165
),
128-
getLocations(utExecution, classFqn),
166+
getLocations(method, utExecution, classFqn),
129167
getRelatedLocations(utExecution),
130168
getCodeFlows(method, utExecution, executionFailure)
131169
)
@@ -146,16 +184,23 @@ class SarifReport(
146184
return Pair(sarifResult, sarifRule)
147185
}
148186

149-
private fun getLocations(utExecution: UtExecution, classFqn: String?): List<SarifPhysicalLocationWrapper> {
187+
private fun getLocations(
188+
method: ExecutableId,
189+
utExecution: UtExecution,
190+
classFqn: String?
191+
): List<SarifLocationWrapper> {
150192
if (classFqn == null)
151193
return listOf()
152-
val sourceRelativePath = sourceFinding.getSourceRelativePath(classFqn)
153-
val startLine = getLastLineNumber(utExecution, classFqn) ?: defaultLineNumber
154-
val sourceCode = sourceFinding.getSourceFile(classFqn)?.readText() ?: ""
194+
val (startLine, classWithErrorFqn) = getLastLineNumberWithClassFqn(method, utExecution, classFqn)
195+
val sourceCode = sourceFinding.getSourceFile(classWithErrorFqn)?.readText() ?: ""
155196
val sourceRegion = SarifRegion.withStartLine(sourceCode, startLine)
197+
val sourceRelativePath = sourceFinding.getSourceRelativePath(classWithErrorFqn)
156198
return listOf(
157199
SarifPhysicalLocationWrapper(
158200
SarifPhysicalLocation(SarifArtifact(sourceRelativePath), sourceRegion)
201+
),
202+
SarifLogicalLocationsWrapper(
203+
listOf(SarifLogicalLocation(classWithErrorFqn)) // class name without method name
159204
)
160205
)
161206
}
@@ -181,31 +226,9 @@ class SarifReport(
181226
utExecution: UtExecution,
182227
executionFailure: UtExecutionFailure
183228
): List<SarifCodeFlow> {
184-
/* Example of a typical stack trace:
185-
- java.lang.Math.multiplyExact(Math.java:867)
186-
- com.abc.Util.multiply(Util.java:10)
187-
- com.abc.Util.multiply(Util.java:6)
188-
- com.abc.Main.example(Main.java:11) // <- `lastMethodCallIndex`
189-
- sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
190-
- ...
191-
*/
192-
val stackTrace = executionFailure.exception.stackTrace
193-
194-
val lastMethodCallIndex = stackTrace.indexOfLast {
195-
it.className == method.classId.name && it.methodName == method.name
196-
}
197-
if (lastMethodCallIndex == -1)
198-
return listOf()
199-
200-
val stackTraceFiltered = stackTrace
201-
.take(lastMethodCallIndex + 1) // taking all elements before the last `method` call
202-
.filter {
203-
!it.className.startsWith("org.utbot.") // filter all internal calls
204-
}
205-
206-
val stackTraceResolved = stackTraceFiltered.mapNotNull {
207-
findStackTraceElementLocation(it)
208-
}.toMutableList()
229+
val stackTraceResolved = filterStackTrace(method, utExecution, executionFailure)
230+
.mapNotNull { findStackTraceElementLocation(it) }
231+
.toMutableList()
209232
if (stackTraceResolved.isEmpty())
210233
return listOf() // empty stack trace is not shown
211234

@@ -235,6 +258,40 @@ class SarifReport(
235258
)
236259
}
237260

261+
private fun filterStackTrace(
262+
method: ExecutableId,
263+
utExecution: UtExecution,
264+
executionFailure: UtExecutionFailure
265+
): List<StackTraceElement> {
266+
/* Example of a typical stack trace:
267+
- java.lang.Math.multiplyExact(Math.java:867)
268+
- com.abc.Util.multiply(Util.java:10)
269+
- com.abc.Util.multiply(Util.java:6)
270+
- com.abc.Main.example(Main.java:11) // <- `lastMethodCallIndex`
271+
- sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
272+
- ...
273+
*/
274+
var stackTrace = executionFailure.exception.stackTrace.toList()
275+
276+
val lastMethodCallIndex = stackTrace.indexOfLast {
277+
it.className == method.classId.name && it.methodName == method.name
278+
}
279+
if (lastMethodCallIndex != -1) {
280+
// taking all elements before the last `method` call
281+
stackTrace = stackTrace.take(lastMethodCallIndex + 1)
282+
}
283+
284+
if (executionFailure.exception is StackOverflowError) {
285+
stackTrace = stackTrace.takeLast(stackTraceLengthForStackOverflow)
286+
}
287+
288+
val stackTraceFiltered = stackTrace.filter {
289+
!it.className.startsWith("org.utbot.") // filter all internal calls
290+
}
291+
292+
return stackTraceFiltered
293+
}
294+
238295
private fun findStackTraceElementLocation(stackTraceElement: StackTraceElement): SarifFlowLocationWrapper? {
239296
val lineNumber = stackTraceElement.lineNumber
240297
if (lineNumber < 1)
@@ -327,15 +384,21 @@ class SarifReport(
327384
}
328385

329386
/**
330-
* Returns the number of the last line in the execution path which is located in the [classFqn].
387+
* Returns the number of the last line in the execution path
388+
* And the name of the class in which it is located.
331389
*/
332-
private fun getLastLineNumber(utExecution: UtExecution, classFqn: String): Int? {
333-
val classFqnPath = classFqn.replace(".", "/")
390+
private fun getLastLineNumberWithClassFqn(
391+
method: ExecutableId,
392+
utExecution: UtExecution,
393+
defaultClassFqn: String
394+
): Pair<Int, String> {
334395
val coveredInstructions = utExecution.coverage?.coveredInstructions
335-
val lastCoveredInstruction = coveredInstructions?.lastOrNull { it.className == classFqnPath }
336-
?: coveredInstructions?.lastOrNull()
396+
val lastCoveredInstruction = coveredInstructions?.lastOrNull()
337397
if (lastCoveredInstruction != null)
338-
return lastCoveredInstruction.lineNumber
398+
return Pair(
399+
lastCoveredInstruction.lineNumber,
400+
lastCoveredInstruction.className.replace('/', '.')
401+
)
339402

340403
// if for some reason we can't extract the last line from the coverage
341404
val lastPathElementLineNumber = try {
@@ -345,7 +408,20 @@ class SarifReport(
345408
} catch (t: Throwable) {
346409
null
347410
}
348-
return lastPathElementLineNumber
411+
if (lastPathElementLineNumber != null) {
412+
return Pair(lastPathElementLineNumber, defaultClassFqn)
413+
}
414+
415+
val methodDefinitionLine = getMethodDefinitionLineNumber(method)
416+
return Pair(methodDefinitionLine ?: defaultLineNumber, defaultClassFqn)
417+
}
418+
419+
private fun getMethodDefinitionLineNumber(method: ExecutableId): Int? {
420+
val sourceFile = sourceFinding.getSourceFile(method.classId.canonicalName)
421+
val lineNumber = sourceFile?.readLines()?.indexOfFirst { line ->
422+
line.contains(" ${method.name}(") // method definition
423+
}
424+
return if (lineNumber == null || lineNumber == -1) null else lineNumber + 1 // to one-based
349425
}
350426

351427
private fun shouldProcessExecutionResult(result: UtExecutionResult): Boolean {

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.intellij.ide.fileTemplates.FileTemplateUtil
88
import com.intellij.ide.fileTemplates.JavaTemplateUtil
99
import com.intellij.ide.highlighter.JavaFileType
1010
import com.intellij.openapi.application.ApplicationManager
11-
import com.intellij.openapi.application.readAction
1211
import com.intellij.openapi.application.runReadAction
1312
import com.intellij.openapi.application.runWriteAction
1413
import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction
@@ -53,7 +52,6 @@ import org.jetbrains.kotlin.idea.core.ShortenReferences
5352
import org.jetbrains.kotlin.idea.core.getPackage
5453
import org.jetbrains.kotlin.idea.core.util.toPsiDirectory
5554
import org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl
56-
import org.jetbrains.kotlin.idea.util.projectStructure.allModules
5755
import org.jetbrains.kotlin.name.FqName
5856
import org.jetbrains.kotlin.psi.KtClass
5957
import org.jetbrains.kotlin.psi.KtNamedFunction
@@ -96,8 +94,7 @@ import org.utbot.intellij.plugin.util.RunConfigurationHelper
9694
import org.utbot.intellij.plugin.util.assertIsDispatchThread
9795
import org.utbot.intellij.plugin.util.assertIsWriteThread
9896
import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested
99-
import org.utbot.sarif.Sarif
100-
import org.utbot.sarif.SarifReport
97+
import org.utbot.sarif.*
10198

10299
object CodeGenerationController {
103100
private val logger = KotlinLogging.logger {}
@@ -211,7 +208,7 @@ object CodeGenerationController {
211208
return
212209
}
213210
UnitTestBotInspectionManager
214-
.getInstance(model.project, srcClassPathToSarifReport)
211+
.getInstance(model.project, SarifReport.minimizeSarifResults(srcClassPathToSarifReport))
215212
.createNewGlobalContext()
216213
.doInspections(AnalysisScope(model.project))
217214
}

0 commit comments

Comments
 (0)