diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt index 2c45066c2b7..4b707103b18 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt @@ -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 { + 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) + } } } diff --git a/idea/testData/debugger/exceptionFilter/kt29871/foo_bar.kt b/idea/testData/debugger/exceptionFilter/kt29871/foo_bar.kt new file mode 100644 index 00000000000..ed5c110477f --- /dev/null +++ b/idea/testData/debugger/exceptionFilter/kt29871/foo_bar.kt @@ -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()) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilterTest.kt b/idea/tests/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilterTest.kt new file mode 100644 index 00000000000..4989e3e6e0d --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilterTest.kt @@ -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 = "" + private val myPathPlug = "" + + private val myTemplates = listOf( + "\tat 1 main.kexe\t\t 0x000000010d7cdb4c kfun:package.function(kotlin.Int) + 108 ($myPathPlug:$mySuffixPlug)\n" + ) + + private var myFiles = HashMap() + private var myExceptionLine: String = "" + + private val mySuffixOptions = listOf( + SuffixOption("10:1", 10, 1), + SuffixOption("14:11", 14, 11), + SuffixOption("", 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) + } + } +} \ No newline at end of file