Skip to content

Support startColumn field in the SARIF report #454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -88,7 +88,7 @@ object PathUtil {
"""<a href="file:///$filePath">${fileName}</a>"""

/**
* 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
Expand Down
17 changes: 16 additions & 1 deletion utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 30 additions & 15 deletions utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -155,14 +155,13 @@ class SarifReport(
}

private fun getRelatedLocations(utExecution: UtExecution): List<SarifRelatedPhysicalLocationWrapper> {
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,
Expand Down Expand Up @@ -228,14 +227,15 @@ 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(
text = stackTraceElement.toString()
),
physicalLocation = SarifPhysicalLocation(
SarifArtifact(relativePath),
SarifRegion(lineNumber)
SarifRegion.withStartLine(sourceCode, lineNumber)
)
)
)
Expand All @@ -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)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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?
}

/**
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down