Add highlighting for exceptions stacktrace on Mac

KT-29817
Review: KOTLIN-CR-2722
This commit is contained in:
Kirill Shmakov
2019-02-15 18:33:14 +03:00
parent 08de82db85
commit 5595a2dd1b
3 changed files with 158 additions and 2 deletions
@@ -19,6 +19,8 @@ package org.jetbrains.kotlin.idea.filters
import com.intellij.execution.filters.*
import com.intellij.execution.filters.impl.HyperlinkInfoFactoryImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
@@ -34,7 +36,7 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter
override fun applyFilter(line: String, entireLength: Int): Filter.Result? {
return runReadAction {
val result = exceptionFilter.applyFilter(line, entireLength)
if (result == null) null else patchResult(result, line)
if (result == null) parseNativeStackTraceLine(line, entireLength) else patchResult(result, line)
}
}
@@ -72,7 +74,7 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter
val file = DebuggerUtils.findSourceFileForClassIncludeLibrarySources(project, searchScope, jvmClassName, fileName)
if (file == null) {
// File can't be found by class name and file name this can happen when smap info is already applied.
// File can't be found by class name and file name: this can happen when smap info is already applied.
// Default filter favours looking for file from class name and that can lead to wrong navigation to inline fun call file and
// line from inline function definition.
val defaultLinkFileNames = defaultResult.resultItems.mapNotNullTo(HashSet()) { (it as? FileHyperlinkInfo)?.descriptor?.file?.name }
@@ -129,11 +131,47 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter
return InlineFunctionHyperLinkInfo(project, inlineInfos)
}
// Extracts file names from Kotlin Native stacktrace strings like
// "\tat 1 main.kexe\t\t 0x000000010d7cdb4c kfun:package.function(kotlin.Int) + 108 (/Users/user.name/repo_name/file_name.kt:10:27)\n"
// The form is encoded in Kotlin Native code and covered by tests witch are linked here.
private fun parseNativeStackTraceLine(rawLine: String, entireLength: Int): Filter.Result? {
val atPrefix = "at "
val ktExtension = ".kt:"
val project = searchScope.project ?: return null
val line = rawLine.trim()
if (!line.startsWith(atPrefix) && !line.endsWith(')')) {
return null
}
val fileNameBegin = line.lastIndexOf('(') + 1
if (fileNameBegin < 1) { // no parentheses => no file name provided
return null
}
val fileNameEnd = line.indexOf(ktExtension, fileNameBegin) + ktExtension.length - 1
if (fileNameEnd < ktExtension.length - 1) { // no kt file extension => not interested
return null
}
val virtualFile = findFile(line.substring(fileNameBegin, fileNameEnd)) ?: return null
val (lineNumber, columnNumber) = parsLineColumn(line.substring(fileNameEnd + 1, line.lastIndex))
val offset = entireLength - rawLine.length + rawLine.indexOf(atPrefix)
val highlightEndOffset = offset + (if (lineNumber > 0) line.lastIndex else fileNameEnd)
val hyperLinkInfo = OpenFileHyperlinkInfo(project, virtualFile, lineNumber, columnNumber)
return Filter.Result(offset + fileNameBegin, highlightEndOffset, hyperLinkInfo)
}
companion object {
// Matches strings like "\tat test.TestPackage$foo$f$1.invoke(a.kt:3)\n"
// or "\tBreakpoint reached at test.TestPackage$foo$f$1.invoke(a.kt:3)\n"
private val STACK_TRACE_ELEMENT_PATTERN = Pattern.compile("^[\\w|\\s]*at\\s+(.+)\\.(.+)\\((.+):(\\d+)\\)\\s*$")
private val LINE_COLUMN_PATTERN = Pattern.compile("(\\d+):(\\d+)")
private fun parseStackTraceLine(line: String): StackTraceElement? {
val matcher = STACK_TRACE_ELEMENT_PATTERN.matcher(line)
if (matcher.matches()) {
@@ -146,5 +184,23 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter
}
return null
}
private fun findFile(fileName: String): VirtualFile? {
if (!DebuggerUtils.isKotlinSourceFile(fileName))
return null
val vfsFileName = FileUtil.toSystemIndependentName(fileName)
return LocalFileSystem.getInstance().findFileByPath(vfsFileName)
}
private fun parsLineColumn(locationLine: String): Pair<Int, Int> {
val matcher = LINE_COLUMN_PATTERN.matcher(locationLine)
if (matcher.matches()) {
val line = Integer.parseInt(matcher.group(1))
val column = Integer.parseInt(matcher.group(2))
return Pair(line, column)
}
return Pair(0, 0)
}
}
}
@@ -0,0 +1,17 @@
package sample
fun hello(): String = "Hello, Kotlin/Native!"
fun bar(count: Int) {
foo(count - 1)
}
fun foo(count: Int) {
if (count <= 0) throw Exception("foo")
bar(count - 1)
}
fun main() {
foo(20)
println(hello())
}
@@ -0,0 +1,83 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.filters
import com.intellij.execution.filters.FileHyperlinkInfo
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.testFramework.LightProjectDescriptor
import org.jetbrains.kotlin.idea.refactoring.toVirtualFile
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.junit.Assert
import java.io.File
private data class SuffixOption(val suffix: String, val expectedLine: Int, val expectedColumn: Int)
class KotlinExceptionFilterTest : KotlinLightCodeInsightFixtureTestCase() {
private val mySuffixPlug = "<suffix>"
private val myPathPlug = "<path>"
private val myTemplates = listOf(
"\tat 1 main.kexe\t\t 0x000000010d7cdb4c kfun:package.function(kotlin.Int) + 108 ($myPathPlug:$mySuffixPlug)\n"
)
private var myFiles = HashMap<String, VirtualFile>()
private var myExceptionLine: String = ""
private val mySuffixOptions = listOf(
SuffixOption("10:1", 10, 1),
SuffixOption("14:11", 14, 11),
SuffixOption("<unknown>", 0, 0)
)
private fun errorMessage(detail: String) = "Failed to parse Kotlin Native exception '$myExceptionLine': $detail"
override fun getProjectDescriptor() = LightProjectDescriptor.EMPTY_PROJECT_DESCRIPTOR
override fun getTestDataPath() = ""
override fun setUp() {
super.setUp()
val rootDir = File("idea/testData/debugger/exceptionFilter/kt29871/")
rootDir.listFiles().forEach {
val virtualFile = it.toVirtualFile()
if (virtualFile != null) {
myFiles[it.absolutePath] = virtualFile
}
}
}
fun testDifferentLocations() {
for (template in myTemplates) {
for (suffixOption in mySuffixOptions) {
val templateWithSuffix = template.replace(mySuffixPlug, suffixOption.suffix)
doTest(templateWithSuffix, suffixOption.expectedLine, suffixOption.expectedColumn)
}
}
}
fun doTest(template: String, expectedLine: Int, expectedColumn: Int) {
val filter = KotlinExceptionFilterFactory().create(GlobalSearchScope.allScope(project))
for ((absolutePath, virtualFile) in myFiles) {
myExceptionLine = template.replace(myPathPlug, absolutePath)
val filterResult = filter.applyFilter(myExceptionLine, myExceptionLine.length)
Assert.assertNotNull(errorMessage("filename is not found by parser"), filterResult)
val fileHyperlinkInfo = filterResult?.firstHyperlinkInfo as FileHyperlinkInfo
val document = FileDocumentManager.getInstance().getDocument(virtualFile)
Assert.assertNotNull(errorMessage("test file $absolutePath could not be found in repository"), document)
val expectedOffset = document!!.getLineStartOffset(expectedLine) + expectedColumn
val descriptor = fileHyperlinkInfo.descriptor
Assert.assertNotNull(errorMessage("found file hyperlink with null descriptor"), descriptor)
Assert.assertEquals(errorMessage("different filename parsed"), virtualFile.canonicalPath, descriptor?.file?.canonicalPath)
Assert.assertEquals(errorMessage("different offset parsed"), expectedOffset, descriptor?.offset)
}
}
}