diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt index ca37c7eb122..a4faba207cb 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt @@ -20,24 +20,53 @@ import com.intellij.debugger.SourcePosition import com.intellij.debugger.engine.DebugProcess import com.intellij.debugger.jdi.VirtualMachineProxyImpl import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.compiler.CompilerPaths import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.containers.ConcurrentWeakFactoryMap +import com.intellij.util.containers.ContainerUtil import com.sun.jdi.Location import com.sun.jdi.ReferenceType import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil +import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches +import org.jetbrains.kotlin.idea.refactoring.getLineCount import org.jetbrains.kotlin.idea.refactoring.getLineStartOffset +import org.jetbrains.kotlin.idea.refactoring.toPsiFile +import org.jetbrains.kotlin.idea.util.ProjectRootsUtil import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.load.kotlin.JvmVirtualFileFinder +import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.name.tail import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.resolve.jvm.JvmClassName +import org.jetbrains.kotlin.utils.addToStdlib.check import org.jetbrains.kotlin.utils.getOrPutNullable import org.jetbrains.org.objectweb.asm.* +import java.io.File import java.util.* +fun isInlineFunctionLineNumber(file: VirtualFile, lineNumber: Int, project: Project): Boolean { + if (ProjectRootsUtil.isProjectSourceFile(project, file)) { + val linesInFile = file.toPsiFile(project)?.getLineCount() ?: return false + return lineNumber > linesInFile + } + + return true +} + +fun readDebugBytecodeInfo(project: Project, + jvmName: JvmClassName, + file: VirtualFile): BytecodeDebugInfo? { + return KotlinDebuggerCaches.getOrReadDebugInfoFromBytecode(project, jvmName, file) +} + fun noStrataLineNumber(location: Location, isDexDebug: Boolean, project: Project, preferInlined: Boolean = false): Int { if (isDexDebug) { if (!preferInlined) { @@ -73,13 +102,100 @@ fun getLastLineNumberForLocation(location: Location, project: Project, searchSco return lineMapping.values.firstOrNull { it.contains(lineNumber) }?.last() } -fun readLineNumberTableMapping(bytes: ByteArray): Map>> { +class WeakBytecodeDebugInfoStorage : ConcurrentWeakFactoryMap() { + override fun create(key: BinaryCacheKey): BytecodeDebugInfo? { + val bytes = readClassFileImpl(key.project, key.jvmName, key.file) ?: return null + + val smapData = readDebugInfo(bytes) + val lineNumberMapping = readLineNumberTableMapping(bytes) + + return BytecodeDebugInfo(smapData, lineNumberMapping) + } + override fun createMap(): Map { + return ContainerUtil.createConcurrentWeakKeyWeakValueMap() + } +} + +class BytecodeDebugInfo(val smapData: SmapData?, val lineTableMapping: Map>>) + +data class BytecodeMethodKey(val methodName: String, val signature: String) + +data class BinaryCacheKey(val project: Project, val jvmName: JvmClassName, val file: VirtualFile) + +private fun readClassFileImpl(project: Project, + jvmName: JvmClassName, + file: VirtualFile): ByteArray? { + val fqNameWithInners = jvmName.fqNameForClassNameWithoutDollars.tail(jvmName.packageFqName) + + fun readFromLibrary(): ByteArray? { + if (!ProjectRootsUtil.isLibrarySourceFile(project, file)) return null + + val classId = ClassId(jvmName.packageFqName, Name.identifier(fqNameWithInners.asString())) + + val fileFinder = JvmVirtualFileFinder.SERVICE.getInstance(project) + val classFile = fileFinder.findVirtualFileWithHeader(classId) ?: return null + return classFile.contentsToByteArray() + } + + fun readFromOutput(isForTestClasses: Boolean): ByteArray? { + if (!ProjectRootsUtil.isProjectSourceFile(project, file)) return null + + val module = ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(file) + val outputDir = CompilerPaths.getModuleOutputDirectory(module, /*forTests = */ isForTestClasses) ?: return null + + val className = fqNameWithInners.asString().replace('.', '$') + var classByDirectory = findClassFileByPath(jvmName.packageFqName.asString(), className, outputDir) + + if (classByDirectory == null) { + if (!isForTestClasses) { + return null + } + + val outputModeDirName = outputDir.name + val androidTestOutputDir = outputDir.parent?.parent?.findChild("androidTest")?.findChild(outputModeDirName) ?: return null + + classByDirectory = findClassFileByPath(jvmName.packageFqName.asString(), className, androidTestOutputDir) ?: return null + } + + return classByDirectory.readBytes() + } + + fun readFromSourceOutput(): ByteArray? = readFromOutput(false) + + fun readFromTestOutput(): ByteArray? = readFromOutput(true) + + return readFromLibrary() ?: + readFromSourceOutput() ?: + readFromTestOutput() +} + +private fun findClassFileByPath(packageName: String, className: String, outputDir: VirtualFile): File? { + val outDirFile = File(outputDir.path).check(File::exists) ?: return null + + val parentDirectory = File(outDirFile, packageName.replace(".", File.separator)) + if (!parentDirectory.exists()) return null + + if (ApplicationManager.getApplication().isUnitTestMode) { + val beforeDexFileClassFile = File(parentDirectory, className + ".class.before_dex") + if (beforeDexFileClassFile.exists()) { + return beforeDexFileClassFile + } + } + + val classFile = File(parentDirectory, className + ".class") + if (classFile.exists()) { + return classFile + } + + return null +} + +private fun readLineNumberTableMapping(bytes: ByteArray): Map>> { val lineNumberMapping = HashMap>>() ClassReader(bytes).accept(object : ClassVisitor(InlineCodegenUtil.API) { override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor? { if (name == null || desc == null) { - // TODO: check constructors return null } @@ -113,7 +229,7 @@ internal fun getOriginalPositionOfInlinedLine(location: Location, project: Proje return mapStacktraceLineToSource(smapData, lineNumber, project, SourceLineKind.EXECUTED_LINE, searchScope) } -internal fun findAndReadClassFile( +private fun findAndReadClassFile( fqName: FqName, fileName: String, project: Project, searchScope: GlobalSearchScope, fileFilter: (VirtualFile) -> Boolean): BytecodeDebugInfo? { val internalName = fqName.asString().replace('.', '/') @@ -124,7 +240,7 @@ internal fun findAndReadClassFile( val virtualFile = file.virtualFile ?: return null if (!fileFilter(virtualFile)) return null - return readClassFile(project, jvmClassName, virtualFile) + return readDebugBytecodeInfo(project, jvmClassName, virtualFile) } internal fun getLocationsOfInlinedLine(type: ReferenceType, position: SourcePosition, sourceSearchScope: GlobalSearchScope): List { @@ -156,7 +272,7 @@ private fun inlinedLinesNumbers( val virtualFile = file.virtualFile ?: return listOf() - val debugInfo = readClassFile(project, jvmClassName, virtualFile) ?: return listOf() + val debugInfo = readDebugBytecodeInfo(project, jvmClassName, virtualFile) ?: return listOf() val smapData = debugInfo.smapData ?: return listOf() val smap = smapData.kotlinStrata ?: return listOf() diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt index 07de78d4a9f..787e7f58970 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt @@ -41,7 +41,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyzeAndGetResult import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult import org.jetbrains.kotlin.idea.debugger.BinaryCacheKey import org.jetbrains.kotlin.idea.debugger.BytecodeDebugInfo -import org.jetbrains.kotlin.idea.debugger.WeakConcurrentBinaryStorage +import org.jetbrains.kotlin.idea.debugger.WeakBytecodeDebugInfoStorage import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.psi.KtCodeFragment import org.jetbrains.kotlin.psi.KtElement @@ -74,10 +74,10 @@ class KotlinDebuggerCaches(project: Project) { PsiModificationTracker.MODIFICATION_COUNT) }, false) - private val binaryCache = CachedValuesManager.getManager(project).createCachedValue( + private val debugInfoCache = CachedValuesManager.getManager(project).createCachedValue( { - CachedValueProvider.Result( - WeakConcurrentBinaryStorage(), + CachedValueProvider.Result( + WeakBytecodeDebugInfoStorage(), PsiModificationTracker.MODIFICATION_COUNT) }, false) @@ -166,12 +166,12 @@ class KotlinDebuggerCaches(project: Project) { } } - fun readFileContent( + fun getOrReadDebugInfoFromBytecode( project: Project, jvmName: JvmClassName, file: VirtualFile): BytecodeDebugInfo? { val cache = getInstance(project) - return cache.binaryCache.value[BinaryCacheKey(project, jvmName, file)] + return cache.debugInfoCache.value[BinaryCacheKey(project, jvmName, file)] } private fun getElementToCreateTypeMapperForLibraryFile(element: PsiElement?) = diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt index e7132fbb88f..b9c1fc426a8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/smapUtil.kt @@ -16,136 +16,16 @@ package org.jetbrains.kotlin.idea.debugger -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.compiler.CompilerPaths import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope -import com.intellij.util.containers.ConcurrentWeakFactoryMap -import com.intellij.util.containers.ContainerUtil import org.jetbrains.kotlin.codegen.inline.FileMapping import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil import org.jetbrains.kotlin.codegen.inline.SMAP import org.jetbrains.kotlin.codegen.inline.SMAPParser -import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches -import org.jetbrains.kotlin.idea.refactoring.getLineCount -import org.jetbrains.kotlin.idea.refactoring.toPsiFile -import org.jetbrains.kotlin.idea.util.ProjectRootsUtil -import org.jetbrains.kotlin.load.kotlin.JvmVirtualFileFinder -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.name.tail import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.jvm.JvmClassName -import org.jetbrains.kotlin.utils.addToStdlib.check import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassVisitor -import java.io.File - -fun isInlineFunctionLineNumber(file: VirtualFile, lineNumber: Int, project: Project): Boolean { - if (ProjectRootsUtil.isProjectSourceFile(project, file)) { - val linesInFile = file.toPsiFile(project)?.getLineCount() ?: return false - return lineNumber > linesInFile - } - - return true -} - -class WeakConcurrentBinaryStorage : ConcurrentWeakFactoryMap() { - override fun create(key: BinaryCacheKey): BytecodeDebugInfo? { - val bytes = readClassFileImpl(key.project, key.jvmName, key.file) ?: return null - - val smapData = readDebugInfo(bytes) - val lineNumberMapping = readLineNumberTableMapping(bytes) - - return BytecodeDebugInfo(smapData, lineNumberMapping) - } - - override fun createMap(): Map { - return ContainerUtil.createConcurrentWeakKeyWeakValueMap() - } -} - -fun readClassFile(project: Project, - jvmName: JvmClassName, - file: VirtualFile): BytecodeDebugInfo? { - return KotlinDebuggerCaches.readFileContent(project, jvmName, file) -} - -class BytecodeDebugInfo(val smapData: SmapData?, val lineTableMapping: Map>>) - -data class BytecodeMethodKey(val methodName: String, val signature: String) -data class BinaryCacheKey(val project: Project, val jvmName: JvmClassName, val file: VirtualFile) - -private fun readClassFileImpl(project: Project, - jvmName: JvmClassName, - file: VirtualFile): ByteArray? { - val fqNameWithInners = jvmName.fqNameForClassNameWithoutDollars.tail(jvmName.packageFqName) - - fun readFromLibrary(): ByteArray? { - if (!ProjectRootsUtil.isLibrarySourceFile(project, file)) return null - - val classId = ClassId(jvmName.packageFqName, Name.identifier(fqNameWithInners.asString())) - - val fileFinder = JvmVirtualFileFinder.SERVICE.getInstance(project) - val classFile = fileFinder.findVirtualFileWithHeader(classId) ?: return null - return classFile.contentsToByteArray() - } - - fun readFromOutput(isForTestClasses: Boolean): ByteArray? { - if (!ProjectRootsUtil.isProjectSourceFile(project, file)) return null - - val module = ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(file) - val outputDir = CompilerPaths.getModuleOutputDirectory(module, /*forTests = */ isForTestClasses) ?: return null - - val className = fqNameWithInners.asString().replace('.', '$') - var classByDirectory = findClassFileByPath(jvmName.packageFqName.asString(), className, outputDir) - - if (classByDirectory == null) { - if (!isForTestClasses) { - return null - } - - val outputModeDirName = outputDir.name - val androidTestOutputDir = outputDir.parent?.parent?.findChild("androidTest")?.findChild(outputModeDirName) ?: return null - - classByDirectory = findClassFileByPath(jvmName.packageFqName.asString(), className, androidTestOutputDir) ?: return null - } - - println("Read file: " + classByDirectory) - return classByDirectory.readBytes() - } - - fun readFromSourceOutput(): ByteArray? = readFromOutput(false) - - fun readFromTestOutput(): ByteArray? = readFromOutput(true) - - return readFromLibrary() ?: - readFromSourceOutput() ?: - readFromTestOutput() -} - -private fun findClassFileByPath(packageName: String, className: String, outputDir: VirtualFile): File? { - val outDirFile = File(outputDir.path).check(File::exists) ?: return null - - val parentDirectory = File(outDirFile, packageName.replace(".", File.separator)) - if (!parentDirectory.exists()) return null - - if (ApplicationManager.getApplication().isUnitTestMode) { - val beforeDexFileClassFile = File(parentDirectory, className + ".class.before_dex") - if (beforeDexFileClassFile.exists()) { - return beforeDexFileClassFile - } - } - - val classFile = File(parentDirectory, className + ".class") - if (classFile.exists()) { - return classFile - } - - return null -} enum class SourceLineKind { CALL_LINE, diff --git a/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt b/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt index f5e60e94c32..4b4ffa68a3c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt +++ b/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt @@ -75,7 +75,7 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter private fun createHyperlinks(jvmName: JvmClassName, file: VirtualFile, line: Int, project: Project): InlineFunctionHyperLinkInfo? { if (!isInlineFunctionLineNumber(file, line, project)) return null - val debugInfo = readClassFile(project, jvmName, file) ?: return null + val debugInfo = readDebugBytecodeInfo(project, jvmName, file) ?: return null val smapData = debugInfo.smapData ?: return null val inlineInfos = arrayListOf()