Add highlighting for exceptions stacktrace on Mac
KT-29817 Review: KOTLIN-CR-2722
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user