Move out JVM debugger functionality

This commit is contained in:
Yan Zhulanow
2019-04-05 15:10:14 +03:00
parent 5843336d42
commit ae7550c5af
318 changed files with 1068 additions and 723 deletions
@@ -0,0 +1,24 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
compile(project(":compiler:backend"))
compile(project(":compiler:backend-common"))
compile(project(":compiler:light-classes"))
compile(project(":idea:idea-core"))
compile(project(":idea:ide-common"))
compile(project(":idea:jvm-debugger:jvm-debugger-util"))
compile(files("${System.getProperty("java.home")}/../lib/tools.jar"))
compileOnly(intellijPluginDep("stream-debugger"))
testCompile(project(":kotlin-test:kotlin-test-junit"))
testCompile(commonDep("junit:junit"))
}
sourceSets {
"main" { projectDefault() }
"test" { none() }
}
@@ -0,0 +1,331 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.DebuggerUtils
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.ObjectCollectedException
import com.sun.jdi.ReferenceType
import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousClass
import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousClassOrNull
import org.jetbrains.kotlin.descriptors.ScriptDescriptor
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.debugger.breakpoints.getLambdasAtLineIfAny
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.Companion.getOrComputeClassNames
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames.Companion.Cached
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames.Companion.EMPTY
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames.Companion.NonCached
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.isObjectLiteral
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.org.objectweb.asm.Type
import java.util.*
class DebuggerClassNameProvider(
private val debugProcess: DebugProcess,
val findInlineUseSites: Boolean = true,
val alwaysReturnLambdaParentClass: Boolean = true
) {
companion object {
private val CLASS_ELEMENT_TYPES = arrayOf<Class<out PsiElement>>(
KtFile::class.java,
KtClassOrObject::class.java,
KtProperty::class.java,
KtNamedFunction::class.java,
KtFunctionLiteral::class.java,
KtAnonymousInitializer::class.java
)
internal fun getRelevantElement(element: PsiElement?): PsiElement? {
if (element == null) {
return null
}
for (elementType in CLASS_ELEMENT_TYPES) {
if (elementType.isInstance(element)) {
return element
}
}
// Do not copy the array (*elementTypes) if the element is one we look for
return runReadAction { PsiTreeUtil.getNonStrictParentOfType(element, *CLASS_ELEMENT_TYPES) }
}
}
private val inlineUsagesSearcher = InlineCallableUsagesSearcher(debugProcess)
/**
* Returns classes in which the given line number *is* present.
*/
fun getClassesForPosition(position: SourcePosition): List<ReferenceType> = with(debugProcess) {
val lineNumber = runReadAction { position.line }
return doGetClassesForPosition(position)
.flatMap { className -> virtualMachineProxy.classesByName(className) }
.flatMap { referenceType -> findTargetClasses(referenceType, lineNumber) }
}
/**
* Returns classes names in JDI format (my.app.App$Nested) in which the given line number *may be* present.
*/
fun getOuterClassNamesForPosition(position: SourcePosition): List<String> {
return doGetClassesForPosition(position).toList()
}
private fun doGetClassesForPosition(position: SourcePosition): Set<String> {
val relevantElement = runReadAction {
position.elementAt?.let { getRelevantElement(it) }
}
val result = getOrComputeClassNames(relevantElement) { element ->
getOuterClassNamesForElement(element)
}.toMutableSet()
for (lambda in position.readAction(::getLambdasAtLineIfAny)) {
result += getOrComputeClassNames(lambda) { element ->
getOuterClassNamesForElement(element)
}
}
return result
}
@PublishedApi
@Suppress("NON_TAIL_RECURSIVE_CALL")
internal tailrec fun getOuterClassNamesForElement(element: PsiElement?): ComputedClassNames {
if (element == null) return EMPTY
return when (element) {
is KtScript -> {
getClassType(element)?.let { return Cached(it) }
return EMPTY
}
is KtFile -> {
val fileClassName = runReadAction { JvmFileClassUtil.getFileClassInternalName(element) }.toJdiName()
Cached(fileClassName)
}
is KtClassOrObject -> {
val enclosingElementForLocal = runReadAction { KtPsiUtil.getEnclosingElementForLocalDeclaration(element) }
when {
enclosingElementForLocal != null ->
// A local class
getOuterClassNamesForElement(enclosingElementForLocal)
runReadAction { element.isObjectLiteral() } ->
getOuterClassNamesForElement(element.relevantParentInReadAction)
else ->
// Guaranteed to be non-local class or object
element.readAction { _ ->
if (element is KtClass && runReadAction { element.isInterface() }) {
val name = getNameForNonLocalClass(element)
if (name != null)
Cached(listOf(name, name + JvmAbi.DEFAULT_IMPLS_SUFFIX))
else
EMPTY
} else {
getNameForNonLocalClass(element)?.let { Cached(it) } ?: EMPTY
}
}
}
}
is KtProperty -> {
val nonInlineClasses = if (runReadAction { element.isTopLevel }) {
// Top level property
getOuterClassNamesForElement(element.relevantParentInReadAction)
} else {
val enclosingElementForLocal = runReadAction { KtPsiUtil.getEnclosingElementForLocalDeclaration(element) }
if (enclosingElementForLocal != null) {
// Local class
getOuterClassNamesForElement(enclosingElementForLocal)
} else {
val containingClassOrFile = runReadAction {
PsiTreeUtil.getParentOfType(element, KtFile::class.java, KtClassOrObject::class.java)
}
if (containingClassOrFile is KtObjectDeclaration && containingClassOrFile.isCompanionInReadAction) {
// Properties from the companion object can be placed in the companion object's containing class
(getOuterClassNamesForElement(containingClassOrFile.relevantParentInReadAction) +
getOuterClassNamesForElement(containingClassOrFile)).distinct()
} else if (containingClassOrFile != null) {
getOuterClassNamesForElement(containingClassOrFile)
} else {
getOuterClassNamesForElement(element.relevantParentInReadAction)
}
}
}
if (findInlineUseSites && (
element.isInlineInReadAction ||
runReadAction { element.accessors.any { it.hasModifier(KtTokens.INLINE_KEYWORD) } })
) {
nonInlineClasses + inlineUsagesSearcher.findInlinedCalls(element) { this.getOuterClassNamesForElement(it) }
} else {
return NonCached(nonInlineClasses.classNames)
}
}
is KtNamedFunction -> {
val classNamesOfContainingDeclaration = getOuterClassNamesForElement(element.relevantParentInReadAction)
var nonInlineClasses: ComputedClassNames = classNamesOfContainingDeclaration
if (runReadAction { element.name == null || element.isLocal }) {
val nameOfAnonymousClass = runReadAction { getClassType(element) }
if (nameOfAnonymousClass != null) {
nonInlineClasses += Cached(nameOfAnonymousClass)
}
}
if (!findInlineUseSites || !element.isInlineInReadAction) {
return NonCached(nonInlineClasses.classNames)
}
val inlineCallSiteClasses = inlineUsagesSearcher.findInlinedCalls(element) { this.getOuterClassNamesForElement(it) }
nonInlineClasses + inlineCallSiteClasses
}
is KtAnonymousInitializer -> {
val initializerOwner = runReadAction { element.containingDeclaration }
if (initializerOwner is KtObjectDeclaration && initializerOwner.isCompanionInReadAction) {
return getOuterClassNamesForElement(runReadAction { initializerOwner.containingClassOrObject })
}
getOuterClassNamesForElement(initializerOwner)
}
is KtFunctionLiteral -> {
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(element)
val nonInlinedLambdaClassName = runReadAction {
asmTypeForAnonymousClass(typeMapper.bindingContext, element).internalName.toJdiName()
}
if (!alwaysReturnLambdaParentClass && !InlineUtil.isInlinedArgument(element, typeMapper.bindingContext, true)) {
return Cached(nonInlinedLambdaClassName)
}
Cached(nonInlinedLambdaClassName) + getOuterClassNamesForElement(element.relevantParentInReadAction)
}
else -> getOuterClassNamesForElement(element.relevantParentInReadAction)
}
}
// Should be called in a read action
private fun getClassType(element: KtElement): String? {
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(element)
asmTypeForAnonymousClassOrNull(typeMapper.bindingContext, element)?.let { return it.className }
val descriptor = typeMapper.bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, element]
if (descriptor is ScriptDescriptor) {
return typeMapper.mapClass(descriptor).className
}
if (descriptor != null) {
val containingDeclaration = descriptor.containingDeclaration
if (containingDeclaration is ScriptDescriptor) {
return typeMapper.mapClass(containingDeclaration).className
}
}
return null
}
private fun getNameForNonLocalClass(nonLocalClassOrObject: KtClassOrObject): String? {
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(nonLocalClassOrObject)
val descriptor = typeMapper.bindingContext[BindingContext.CLASS, nonLocalClassOrObject] ?: return null
val type = typeMapper.mapClass(descriptor)
if (type.sort != Type.OBJECT) {
return null
}
return type.className
}
private val KtDeclaration.isInlineInReadAction: Boolean
get() = runReadAction { hasModifier(KtTokens.INLINE_KEYWORD) }
private val KtObjectDeclaration.isCompanionInReadAction: Boolean
get() = runReadAction { isCompanion() }
private val PsiElement.relevantParentInReadAction
get() = runReadAction { getRelevantElement(this.parent) }
}
private fun String.toJdiName() = replace('/', '.')
private fun DebugProcess.findTargetClasses(outerClass: ReferenceType, lineAt: Int): List<ReferenceType> {
val vmProxy = virtualMachineProxy
try {
if (!outerClass.isPrepared) {
return emptyList()
}
} catch (e: ObjectCollectedException) {
return emptyList()
}
val targetClasses = ArrayList<ReferenceType>(1)
try {
for (location in outerClass.allLineLocations()) {
val locationLine = location.lineNumber() - 1
if (locationLine < 0) {
// such locations are not correspond to real lines in code
continue
}
if (lineAt == locationLine) {
val method = location.method()
if (method == null || DebuggerUtils.isSynthetic(method) || method.isBridge) {
// skip synthetic methods
continue
}
targetClasses += outerClass
break
}
}
// The same line number may appear in different classes so we have to scan nested classes as well.
// For example, in the next example line 3 appears in both Foo and Foo$Companion.
/* class Foo {
companion object {
val a = Foo() /* line 3 */
}
} */
val nestedTypes = vmProxy.nestedTypes(outerClass)
for (nested in nestedTypes) {
targetClasses += findTargetClasses(nested, lineAt)
}
} catch (_: AbsentInformationException) {
}
return targetClasses
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
abstract class DelegateSourcePosition(private var delegate: SourcePosition) : SourcePosition() {
override fun getFile(): PsiFile = delegate.file
override fun getElementAt(): PsiElement? = delegate.elementAt
override fun getLine(): Int = delegate.line
override fun getOffset(): Int = delegate.offset
override fun openEditor(requestFocus: Boolean): Editor = delegate.openEditor(requestFocus)
override fun canNavigate() = delegate.canNavigate()
override fun canNavigateToSource() = delegate.canNavigateToSource()
override fun navigate(requestFocus: Boolean) {
delegate.navigate(requestFocus)
}
override fun hashCode(): Int = delegate.hashCode()
override fun equals(other: Any?) = delegate == other
override fun toString() = "DSP($delegate)"
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.resolve.diagnostics.DiagnosticSuppressor
class DiagnosticSuppressorForDebugger : DiagnosticSuppressor {
override fun isSuppressed(diagnostic: Diagnostic): Boolean {
val element = diagnostic.psiElement
val containingFile = element.containingFile
if (containingFile is KtCodeFragment) {
val diagnosticFactory = diagnostic.factory
return diagnosticFactory == Errors.UNSAFE_CALL
}
return false
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.engine.DebugProcess
import com.intellij.openapi.application.ex.ApplicationManagerEx
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.ui.MessageType
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.xdebugger.impl.XDebugSessionImpl
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.idea.debugger.DebuggerClassNameProvider.Companion.getRelevantElement
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames
import org.jetbrains.kotlin.idea.search.usagesSearch.isImportUsage
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.isInsideOf
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.inline.InlineUtil
class InlineCallableUsagesSearcher(private val myDebugProcess: DebugProcess) {
fun findInlinedCalls(
declaration: KtDeclaration,
bindingContext: BindingContext = KotlinDebuggerCaches.getOrCreateTypeMapper(declaration).bindingContext,
transformer: (PsiElement) -> ComputedClassNames
): ComputedClassNames {
if (!checkIfInline(declaration, bindingContext)) {
return ComputedClassNames.EMPTY
}
else {
val searchResult = hashSetOf<PsiElement>()
val declarationName = runReadAction { declaration.name }
val task = Runnable {
ReferencesSearch.search(declaration, getScopeForInlineDeclarationUsages(declaration)).forEach {
if (!runReadAction { it.isImportUsage() }) {
val usage = (it.element as? KtElement)?.let(::getRelevantElement)
if (usage != null && !runReadAction { declaration.isAncestor(usage) }) {
searchResult.add(usage)
}
}
}
}
var isSuccess = true
val applicationEx = ApplicationManagerEx.getApplicationEx()
if (applicationEx.isDispatchThread) {
isSuccess = ProgressManager.getInstance().runProcessWithProgressSynchronously(
task,
"Compute class names for declaration $declarationName",
true,
myDebugProcess.project)
}
else {
ProgressManager.getInstance().runProcess(task, EmptyProgressIndicator())
}
if (!isSuccess) {
XDebugSessionImpl.NOTIFICATION_GROUP.createNotification(
"Debugger can skip some executions of $declarationName because the computation of class names was interrupted",
MessageType.WARNING
).notify(myDebugProcess.project)
}
val results = searchResult.map { transformer(it) }
return ComputedClassNames(results.flatMap { it.classNames }, shouldBeCached = results.all { it.shouldBeCached })
}
}
private fun checkIfInline(declaration: KtDeclaration, bindingContext: BindingContext): Boolean {
val descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, declaration) ?: return false
return when (descriptor) {
is FunctionDescriptor -> InlineUtil.isInline(descriptor)
is PropertyDescriptor -> InlineUtil.hasInlineAccessors(descriptor)
else -> false
}
}
private fun getScopeForInlineDeclarationUsages(inlineDeclaration: KtDeclaration): GlobalSearchScope {
val virtualFile = runReadAction { inlineDeclaration.containingFile.virtualFile }
return if (virtualFile != null && ProjectRootsUtil.isLibraryFile(myDebugProcess.project, virtualFile)) {
myDebugProcess.searchScope.uniteWith(
KotlinSourceFilterScope.librarySources(GlobalSearchScope.allScope(myDebugProcess.project), myDebugProcess.project))
}
else {
myDebugProcess.searchScope
}
}
}
@@ -0,0 +1,170 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.ide.util.ModuleRendererFactory
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications
import com.intellij.ui.components.JBList
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil.findFilesWithExactPackage
import org.jetbrains.kotlin.psi.KtFile
class KotlinAlternativeSourceNotificationProvider(private val myProject: Project) : EditorNotifications.Provider<EditorNotificationPanel>() {
override fun getKey(): Key<EditorNotificationPanel> {
return KEY
}
override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? {
if (!DebuggerSettings.getInstance().SHOW_ALTERNATIVE_SOURCE) {
return null
}
val session = XDebuggerManager.getInstance(myProject).currentSession
if (session == null) {
FILE_PROCESSED_KEY.set(file, null)
return null
}
val position = session.currentPosition
if (file != position?.file) {
FILE_PROCESSED_KEY.set(file, null)
return null
}
if (DumbService.getInstance(myProject).isDumb) return null
val ktFile = PsiManager.getInstance(myProject).findFile(file) as? KtFile ?: return null
val packageFqName = ktFile.packageFqName
val fileName = ktFile.name
val alternativeKtFiles = findFilesWithExactPackage(packageFqName, GlobalSearchScope.allScope(myProject), myProject).filterTo(HashSet()) {
it.name == fileName
}
FILE_PROCESSED_KEY.set(file, true)
if (alternativeKtFiles.size <= 1) {
return null
}
val currentFirstAlternatives: Collection<KtFile> = listOf(ktFile) + alternativeKtFiles.filter { it != ktFile }
val frame = session.currentStackFrame
val locationDeclName: String? = when (frame) {
is JavaStackFrame -> {
val location = frame.descriptor.location
location?.declaringType()?.name()
}
else -> null
}
return AlternativeSourceNotificationPanel(currentFirstAlternatives, myProject, file, locationDeclName)
}
private class AlternativeSourceNotificationPanel(
alternatives: Collection<KtFile>,
project: Project,
file: VirtualFile,
locationDeclName: String?
) : EditorNotificationPanel() {
private class ComboBoxFileElement(val ktFile: KtFile) {
private val label: String by lazy(LazyThreadSafetyMode.NONE) {
val factory = ModuleRendererFactory.findInstance(ktFile)
val moduleRenderer = factory.moduleRenderer
moduleRenderer.getListCellRendererComponent(ourDummyList, ktFile, 1, false, false)
moduleRenderer.text ?: ""
}
override fun toString(): String = label
companion object {
private val ourDummyList = JBList<KtFile>()
}
}
init {
setText("Alternative source available for file ${file.name}")
val items = alternatives.map { ComboBoxFileElement(it) }
myLinksPanel.add(
ComboBox<ComboBoxFileElement>(items.toTypedArray()).apply {
addActionListener {
val context = DebuggerManagerEx.getInstanceEx(project).context
val session = context.debuggerSession
val ktFile = (selectedItem as ComboBoxFileElement).ktFile
val vFile = ktFile.containingFile.virtualFile
when {
session != null && vFile != null ->
session.process.managerThread.schedule(object : DebuggerCommandImpl() {
override fun action() {
if (!StringUtil.isEmpty(locationDeclName)) {
DebuggerUtilsEx.setAlternativeSourceUrl(locationDeclName, vFile.url, project)
}
DebuggerUIUtil.invokeLater {
FileEditorManager.getInstance(project).closeFile(file)
session.refresh(true)
}
}
})
else -> {
FileEditorManager.getInstance(project).closeFile(file)
ktFile.navigate(true)
}
}
}
})
createActionLabel("Disable") {
DebuggerSettings.getInstance().SHOW_ALTERNATIVE_SOURCE = false
FILE_PROCESSED_KEY.set(file, null)
val fileEditorManager = FileEditorManager.getInstance(project)
val editor = fileEditorManager.getSelectedEditor(file)
if (editor != null) {
fileEditorManager.removeTopComponent(editor, this)
}
}
}
}
companion object {
private val KEY = Key.create<EditorNotificationPanel>("KotlinAlternativeSource")
// FIXME: Share AlternativeSourceNotificationProvider.FILE_PROCESSED_KEY
@Suppress("UNCHECKED_CAST", "DEPRECATION")
private val FILE_PROCESSED_KEY = Key.findKeyByName("AlternativeSourceCheckDone") as Key<Boolean>
}
}
@@ -0,0 +1,190 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.xdebugger.frame.XNamedValue
import com.sun.jdi.*
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceProviderBase {
private companion object {
const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt"
tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? {
if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") {
return type
}
return findBaseContinuationSuperSupertype(type.superclass() ?: return null)
}
}
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
return getAsyncStackTrace(stackFrame.stackFrameProxy, suspendContext)
}
fun getAsyncStackTrace(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
val location = frameProxy.location()
if (!location.isInKotlinSources()) {
return null
}
val method = location.safeMethod() ?: return null
val threadReference = frameProxy.threadProxy().threadReference
if (threadReference == null || !threadReference.isSuspended || !suspendContext.debugProcess.canRunEvaluation) {
return null
}
val evaluationContext = EvaluationContextImpl(suspendContext, frameProxy)
val context = ExecutionContext(evaluationContext, frameProxy)
// DebugMetadataKt not found, probably old kotlin-stdlib version
val debugMetadataKtType = context.findClassSafe(DEBUG_METADATA_KT) ?: return null
val asyncContext = AsyncStackTraceContext(context, method, debugMetadataKtType)
return asyncContext.getAsyncStackTraceForSuspendLambda() ?: asyncContext.getAsyncStackTraceForSuspendFunction()
}
private fun AsyncStackTraceContext.getAsyncStackTraceForSuspendLambda(): List<StackFrameItem>? {
if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") {
return null
}
val thisObject = context.frameProxy.thisObject() ?: return null
val thisType = thisObject.referenceType()
if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) {
return null
}
return collectFrames(thisObject)
}
private fun AsyncStackTraceContext.getAsyncStackTraceForSuspendFunction(): List<StackFrameItem>? {
if ("Lkotlin/coroutines/Continuation;)" !in method.signature()) {
return null
}
val frameProxy = context.frameProxy
val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null
val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null
return collectFrames(continuation)
}
private fun AsyncStackTraceContext.collectFrames(continuation: ObjectReference): List<StackFrameItem>? {
val frames = mutableListOf<StackFrameItem>()
collectFramesRecursively(continuation, frames)
return frames
}
private fun AsyncStackTraceContext.collectFramesRecursively(continuation: ObjectReference, consumer: MutableList<StackFrameItem>) {
val continuationType = continuation.referenceType() as? ClassType ?: return
val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return
val location = getLocation(continuation)
val spilledVariables = getSpilledVariables(continuation) ?: emptyList()
if (location != null) {
consumer += StackFrameItem(location, spilledVariables)
}
val completionField = baseContinuationSupertype.fieldByName("completion") ?: return
val completion = continuation.getValue(completionField) as? ObjectReference ?: return
collectFramesRecursively(completion, consumer)
}
private fun AsyncStackTraceContext.getLocation(continuation: ObjectReference): Location? {
val getStackTraceElementMethod = debugMetadataKtType.methodsByName(
"getStackTraceElement",
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)Ljava/lang/StackTraceElement;"
).firstOrNull() ?: return null
val args = listOf(continuation)
val stackTraceElement = context.invokeMethod(debugMetadataKtType, getStackTraceElementMethod, args) as? ObjectReference
?: return null
val stackTraceElementType = stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name }
?: return null
fun getValue(name: String, desc: String): Value? {
val method = stackTraceElementType.methodsByName(name, desc).single()
return context.invokeMethod(stackTraceElement, method, emptyList())
}
val className = (getValue("getClassName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null
val methodName = (getValue("getMethodName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null
val lineNumber = (getValue("getLineNumber", "()I") as? IntegerValue)?.value()?.takeIf { it >= 0 } ?: return null
val locationClass = context.findClassSafe(className) ?: return null
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
}
private fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName(
"getSpilledVariableFieldMapping",
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;"
).firstOrNull() ?: return null
val args = listOf(continuation)
val rawSpilledVariables = context.invokeMethod(debugMetadataKtType, getSpilledVariableFieldMappingMethod, args) as? ArrayReference
?: return null
val length = rawSpilledVariables.length() / 2
val spilledVariables = ArrayList<XNamedValue>(length)
for (index in 0 until length) {
val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue
val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue
val field = continuation.referenceType().fieldByName(fieldName) ?: continue
val valueDescriptor = object : ValueDescriptorImpl(context.project) {
override fun calcValueName() = variableName
override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field)
override fun getDescriptorEvaluation(context: DebuggerContext?) =
throw EvaluateException("Spilled variable evaluation is not supported")
}
spilledVariables += JavaValue.create(
null,
valueDescriptor,
context.evaluationContext,
context.debugProcess.xdebugProcess!!.nodeManager,
false
)
}
return spilledVariables
}
private fun ExecutionContext.findClassSafe(className: String): ClassType? {
return try {
findClass(className) as? ClassType
} catch (e: Throwable) {
null
}
}
private class AsyncStackTraceContext(
val context: ExecutionContext,
val method: Method,
val debugMetadataKtType: ClassType
)
}
@@ -0,0 +1,15 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger
import com.intellij.debugger.engine.AsyncStackTraceProvider
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.memory.utils.StackFrameItem
interface KotlinCoroutinesAsyncStackTraceProviderBase : AsyncStackTraceProvider {
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>?
}
@@ -0,0 +1,14 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.memory.utils.StackFrameItem
interface KotlinCoroutinesAsyncStackTraceProviderBase {
fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>?
}
@@ -0,0 +1,56 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SimpleConfigurable
import com.intellij.openapi.util.Getter
import com.intellij.util.xmlb.XmlSerializerUtil
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.settings.DebuggerSettingsCategory
import com.intellij.xdebugger.settings.XDebuggerSettings
import org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingConfigurableUi
@State(name = "KotlinDebuggerSettings", storages = arrayOf(Storage("kotlin_debug.xml")))
class KotlinDebuggerSettings : XDebuggerSettings<KotlinDebuggerSettings>("kotlin_debugger"), Getter<KotlinDebuggerSettings> {
var DEBUG_RENDER_DELEGATED_PROPERTIES: Boolean = true
var DEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES: Boolean = true
var DEBUG_IS_FILTER_FOR_STDLIB_ALREADY_ADDED: Boolean = false
companion object {
fun getInstance(): KotlinDebuggerSettings {
return XDebuggerUtil.getInstance()?.getDebuggerSettings(KotlinDebuggerSettings::class.java)!!
}
}
override fun createConfigurables(category: DebuggerSettingsCategory): Collection<Configurable?> {
return when (category) {
DebuggerSettingsCategory.STEPPING ->
listOf(SimpleConfigurable.create(
"reference.idesettings.debugger.kotlin.stepping",
"Kotlin",
KotlinSteppingConfigurableUi::class.java,
this))
DebuggerSettingsCategory.DATA_VIEWS ->
listOf(SimpleConfigurable.create(
"reference.idesettings.debugger.kotlin.data.view",
"Kotlin",
KotlinDelegatedPropertyRendererConfigurableUi::class.java,
this))
else -> listOf()
}
}
override fun getState() = this
override fun get() = this
override fun loadState(state: KotlinDebuggerSettings) {
XmlSerializerUtil.copyBean<KotlinDebuggerSettings>(state, this)
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.KotlinDelegatedPropertyRendererConfigurableUi">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="99d36" class="javax.swing.JCheckBox" binding="renderDelegatedProperties">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="false"/>
<text resource-bundle="org/jetbrains/kotlin/idea/KotlinBundle" key="debugger.data.view.delegated.properties"/>
</properties>
</component>
<vspacer id="c37da">
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger;
import com.intellij.openapi.options.ConfigurableUi;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class KotlinDelegatedPropertyRendererConfigurableUi implements ConfigurableUi<KotlinDebuggerSettings> {
private JCheckBox renderDelegatedProperties;
private JPanel myPanel;
@Override
public void reset(@NotNull KotlinDebuggerSettings settings) {
boolean flag = settings.getDEBUG_RENDER_DELEGATED_PROPERTIES();
renderDelegatedProperties.setSelected(flag);
}
@Override
public boolean isModified(@NotNull KotlinDebuggerSettings settings) {
return settings.getDEBUG_RENDER_DELEGATED_PROPERTIES() != renderDelegatedProperties.isSelected();
}
@Override
public void apply(@NotNull KotlinDebuggerSettings settings) {
settings.setDEBUG_RENDER_DELEGATED_PROPERTIES(renderDelegatedProperties.isSelected());
}
@NotNull
@Override
public JComponent getComponent() {
return myPanel;
}
}
@@ -0,0 +1,196 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.FrameExtraVariablesProvider
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.evaluation.TextWithImportsImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.text.CharArrayUtil
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineEndOffset
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import java.util.*
import kotlin.math.max
import kotlin.math.min
class KotlinFrameExtraVariablesProvider : FrameExtraVariablesProvider {
override fun isAvailable(sourcePosition: SourcePosition, evalContext: EvaluationContext): Boolean {
if (runReadAction { sourcePosition.line } < 0) return false
return sourcePosition.file.fileType == KotlinFileType.INSTANCE && DebuggerSettings.getInstance().AUTO_VARIABLES_MODE
}
override fun collectVariables(
sourcePosition: SourcePosition, evalContext: EvaluationContext, alreadyCollected: MutableSet<String>): Set<TextWithImports> {
return runReadAction { findAdditionalExpressions(sourcePosition) }
}
}
private fun findAdditionalExpressions(position: SourcePosition): Set<TextWithImports> {
val line = position.line
val file = position.file
val vFile = file.virtualFile
val doc = if (vFile != null) FileDocumentManager.getInstance().getDocument(vFile) else null
if (doc == null || doc.lineCount == 0 || line > (doc.lineCount - 1)) {
return emptySet()
}
val offset = file.getLineStartOffset(line)?.takeIf { it > 0 } ?: return emptySet()
val elem = file.findElementAt(offset) ?: return emptySet()
val containingElement = getContainingElement(elem) ?: elem
val limit = getLineRangeForElement(containingElement, doc)
var startLine = max(limit.startOffset, line)
while (startLine - 1 > limit.startOffset && shouldSkipLine(file, doc, startLine - 1)) {
startLine--
}
var endLine = min(limit.endOffset, line)
while (endLine + 1 < limit.endOffset && shouldSkipLine(file, doc, endLine + 1)) {
endLine++
}
val startOffset = file.getLineStartOffset(startLine) ?: return emptySet()
val endOffset = file.getLineEndOffset(endLine) ?: return emptySet()
if (startOffset >= endOffset) return emptySet()
val lineRange = TextRange(startOffset, endOffset)
if (lineRange.isEmpty) return emptySet()
val expressions = LinkedHashSet<TextWithImports>()
val variablesCollector = VariablesCollector(lineRange, expressions)
containingElement.accept(variablesCollector)
return expressions
}
private fun getContainingElement(element: PsiElement): KtElement? {
val contElement = PsiTreeUtil.getParentOfType(element, KtDeclaration::class.java) ?: PsiTreeUtil.getParentOfType(element, KtElement::class.java)
if (contElement is KtProperty && contElement.isLocal) {
val parent = contElement.parent
return getContainingElement(parent)
}
if (contElement is KtDeclarationWithBody) {
return contElement.bodyExpression
}
return contElement
}
private fun getLineRangeForElement(containingElement: PsiElement, doc: Document): TextRange {
val elemRange = containingElement.textRange
return TextRange(doc.getLineNumber(elemRange.startOffset), doc.getLineNumber(elemRange.endOffset))
}
private fun shouldSkipLine(file: PsiFile, doc: Document, line: Int): Boolean {
val start = CharArrayUtil.shiftForward(doc.charsSequence, doc.getLineStartOffset(line), " \n\t")
val end = doc.getLineEndOffset(line)
if (start >= end) {
return true
}
val elemAtOffset = file.findElementAt(start)
val topmostElementAtOffset = CodeInsightUtils.getTopmostElementAtOffset(elemAtOffset!!, start)
return topmostElementAtOffset !is KtDeclaration
}
private class VariablesCollector(
private val myLineRange: TextRange,
private val myExpressions: MutableSet<TextWithImports>
) : KtTreeVisitorVoid() {
override fun visitKtElement(element: KtElement) {
if (element.isInRange()) {
super.visitKtElement(element)
}
}
override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
if (expression.isInRange()) {
val selector = expression.selectorExpression
if (selector is KtReferenceExpression) {
if (isRefToProperty(selector)) {
myExpressions.add(expression.createText())
return
}
}
}
super.visitQualifiedExpression(expression)
}
private fun isRefToProperty(expression: KtReferenceExpression): Boolean {
// NB: analyze() cannot be called here, because DELEGATED_PROPERTY_RESOLVED_CALL will be always null
// Looks like a bug
@Suppress("DEPRECATION")
val context = expression.analyzeWithAllCompilerChecks().bindingContext
val descriptor = context[BindingContext.REFERENCE_TARGET, expression]
if (descriptor is PropertyDescriptor) {
val getter = descriptor.getter
return (getter == null || context[BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, getter] == null) &&
descriptor.compileTimeInitializer == null
}
return false
}
override fun visitReferenceExpression(expression: KtReferenceExpression) {
if (expression.isInRange()) {
if (isRefToProperty(expression)) {
myExpressions.add(expression.createText())
}
}
super.visitReferenceExpression(expression)
}
private fun KtElement.isInRange(): Boolean = myLineRange.intersects(this.textRange)
private fun KtElement.createText(): TextWithImports = TextWithImportsImpl(CodeFragmentKind.EXPRESSION, this.text)
override fun visitClass(klass: KtClass) {
// Do not show expressions used in local classes
}
override fun visitNamedFunction(function: KtNamedFunction) {
// Do not show expressions used in local functions
}
override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) {
// Do not show expressions used in anonymous objects
}
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
// Do not show expressions used in lambdas
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger;
import com.intellij.xdebugger.breakpoints.XLineBreakpointType;
import com.jetbrains.javascript.debugger.JavaScriptDebugAware;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinLineBreakpointType;
public class KotlinJavaScriptDebugAware extends JavaScriptDebugAware {
@Nullable
@Override
public Class<? extends XLineBreakpointType<?>> getBreakpointTypeClass() {
return KotlinLineBreakpointType.class;
}
}
@@ -0,0 +1,358 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.MultiRequestPositionManager
import com.intellij.debugger.NoDataException
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.PositionManagerEx
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.requests.ClassPrepareRequestor
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.Computable
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.compiled.ClsFileImpl
import com.intellij.psi.search.DelegatingGlobalSearchScope
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.ThreeState
import com.intellij.xdebugger.frame.XStackFrame
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import com.sun.jdi.request.ClassPrepareRequest
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineCount
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.debugger.breakpoints.getLambdasAtLineIfAny
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.decompiler.classFile.KtClsFile
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import com.intellij.debugger.engine.DebuggerUtils as JDebuggerUtils
class KotlinPositionManager(private val myDebugProcess: DebugProcess) : MultiRequestPositionManager, PositionManagerEx() {
private val allKotlinFilesScope =
object : DelegatingGlobalSearchScope(
KotlinSourceFilterScope.projectAndLibrariesSources(GlobalSearchScope.allScope(myDebugProcess.project), myDebugProcess.project)
) {
private val projectIndex = ProjectRootManager.getInstance(myDebugProcess.project).fileIndex
private val scopeComparator =
Comparator.comparing(projectIndex::isInSourceContent)
.thenComparing(projectIndex::isInLibrarySource)
.thenComparing { file1, file2 -> super.compare(file1, file2) }
override fun compare(file1: VirtualFile, file2: VirtualFile): Int {
return scopeComparator.compare(file1, file2)
}
}
private val sourceSearchScopes: List<GlobalSearchScope> = listOf(
myDebugProcess.searchScope,
allKotlinFilesScope
)
override fun getAcceptedFileTypes(): Set<FileType> = KotlinFileTypeFactory.KOTLIN_FILE_TYPES_SET
override fun evaluateCondition(context: EvaluationContext, frame: StackFrameProxyImpl, location: Location, expression: String): ThreeState? {
return ThreeState.UNSURE
}
override fun createStackFrame(frame: StackFrameProxyImpl, debugProcess: DebugProcessImpl, location: Location): XStackFrame? {
if (location.isInKotlinSources()) {
return KotlinStackFrame(frame)
}
return null
}
override fun getSourcePosition(location: Location?): SourcePosition? {
if (location == null) throw NoDataException.INSTANCE
val fileName = location.safeSourceName() ?: throw NoDataException.INSTANCE
val lineNumber = location.safeLineNumber()
if (lineNumber < 0) {
throw NoDataException.INSTANCE
}
if (!DebuggerUtils.isKotlinSourceFile(fileName)) throw NoDataException.INSTANCE
val psiFile = getPsiFileByLocation(location)?.let {
replaceWithAlternativeSource(it, location)
}
if (psiFile == null) {
val isKotlinStrataAvailable = location.declaringType().containsKotlinStrata()
if (isKotlinStrataAvailable) {
try {
val javaSourceFileName = location.sourceName("Java")
val javaClassName = JvmClassName.byInternalName(defaultInternalName(location))
val project = myDebugProcess.project
val defaultPsiFile = DebuggerUtils.findSourceFileForClass(
project, sourceSearchScopes, javaClassName, javaSourceFileName, location)
if (defaultPsiFile != null) {
return SourcePosition.createFromLine(defaultPsiFile, 0)
}
}
catch (e: AbsentInformationException) {
// ignored
}
}
throw NoDataException.INSTANCE
}
if (psiFile !is KtFile) throw NoDataException.INSTANCE
val sourceLineNumber = location.safeSourceLineNumber()
if (sourceLineNumber < 0) {
throw NoDataException.INSTANCE
}
val lambdaOrFunIfInside = getLambdaOrFunIfInside(location, psiFile, sourceLineNumber)
if (lambdaOrFunIfInside != null) {
return SourcePosition.createFromElement(lambdaOrFunIfInside.bodyExpression!!)
}
val elementInDeclaration = getElementForDeclarationLine(location, psiFile, sourceLineNumber)
if (elementInDeclaration != null) {
return SourcePosition.createFromElement(elementInDeclaration)
}
if (sourceLineNumber > psiFile.getLineCount() && myDebugProcess.isDexDebug()) {
val (line, ktFile) = ktLocationInfo(location, true, myDebugProcess.project, false, psiFile)
return SourcePosition.createFromLine(ktFile ?: psiFile, line - 1)
}
val sameLineLocations = location.safeMethod()?.safeAllLineLocations()?.filter {
it.safeLineNumber() == lineNumber && it.safeSourceName() == fileName
}
if (sameLineLocations != null) {
// There're several locations for same source line. If same source position would be created for all of them,
// breakpoints at this line will stop on every location.
// Each location is probably some code in arguments between inlined invocations (otherwise same line locations would
// have been merged into one), but it's impossible to correctly map locations to actual source expressions now.
val locationIndex = sameLineLocations.indexOf(location)
if (locationIndex > 0) {
/*
`finally {}` block code is placed in the class file twice.
Unless the debugger metadata is available, we can't figure out if we are inside `finally {}`, so we have to check it using PSI.
This is conceptually wrong and won't work in some cases, but it's still better than nothing.
*/
val elementAt = psiFile.getLineStartOffset(lineNumber)?.let { psiFile.findElementAt(it) }
val isInsideDuplicatedFinally = elementAt != null && elementAt.getStrictParentOfType<KtFinallySection>() != null
if (!isInsideDuplicatedFinally) {
return KotlinReentrantSourcePosition(SourcePosition.createFromLine(psiFile, sourceLineNumber))
}
}
}
return SourcePosition.createFromLine(psiFile, sourceLineNumber)
}
class KotlinReentrantSourcePosition(delegate: SourcePosition) : DelegateSourcePosition(delegate)
private fun replaceWithAlternativeSource(psiFile: PsiFile, location: Location): PsiFile {
fun findAlternativeSource(): PsiFile? {
val qName = location.declaringType().name()
val alternativeFileUrl = DebuggerUtilsEx.getAlternativeSourceUrl(qName, myDebugProcess.project) ?: return null
val alternativePsiFile = VirtualFileManager.getInstance().findFileByUrl(alternativeFileUrl) ?: return null
return psiFile.manager.findFile(alternativePsiFile)
}
return findAlternativeSource() ?: psiFile
}
// Returns a property or a constructor if debugger stops at class declaration
private fun getElementForDeclarationLine(location: Location, file: KtFile, lineNumber: Int): KtElement? {
val lineStartOffset = file.getLineStartOffset(lineNumber) ?: return null
val elementAt = file.findElementAt(lineStartOffset)
val contextElement = getContextElement(elementAt)
if (contextElement !is KtClass) return null
val methodName = location.method().name()
return when {
JvmAbi.isGetterName(methodName) -> {
val parameterForGetter = contextElement.primaryConstructor?.valueParameters?.firstOrNull {
it.hasValOrVar() && it.name != null && JvmAbi.getterName(it.name!!) == methodName
} ?: return null
parameterForGetter
}
methodName == "<init>" -> contextElement.primaryConstructor
else -> null
}
}
private fun getLambdaOrFunIfInside(location: Location, file: KtFile, lineNumber: Int): KtFunction? {
val currentLocationFqName = location.declaringType().name() ?: return null
val start = CodeInsightUtils.getStartLineOffset(file, lineNumber)
val end = CodeInsightUtils.getEndLineOffset(file, lineNumber)
if (start == null || end == null) return null
val literalsOrFunctions = getLambdasAtLineIfAny(file, lineNumber)
if (literalsOrFunctions.isEmpty()) return null
val elementAt = file.findElementAt(start) ?: return null
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(elementAt)
val currentLocationClassName = JvmClassName.byFqNameWithoutInnerClasses(FqName(currentLocationFqName))
.internalName.replace('/', '.')
for (literal in literalsOrFunctions) {
if (InlineUtil.isInlinedArgument(literal, typeMapper.bindingContext, true)) {
if (isInsideInlineArgument(literal, location, myDebugProcess as DebugProcessImpl, typeMapper.bindingContext)) {
return literal
}
continue
}
val internalClassNames = DebuggerClassNameProvider(myDebugProcess, alwaysReturnLambdaParentClass = false)
.getOuterClassNamesForElement(literal.firstChild)
.classNames
if (internalClassNames.any { it == currentLocationClassName }) {
return literal
}
}
return null
}
private fun getPsiFileByLocation(location: Location): PsiFile? {
val sourceName = location.safeSourceName() ?: return null
val referenceInternalName = try {
if (location.declaringType().containsKotlinStrata()) {
//replace is required for windows
location.sourcePath().replace('\\', '/')
}
else {
defaultInternalName(location)
}
}
catch (e: AbsentInformationException) {
defaultInternalName(location)
}
val className = JvmClassName.byInternalName(referenceInternalName)
val project = myDebugProcess.project
return DebuggerUtils.findSourceFileForClass(project, sourceSearchScopes, className, sourceName, location)
}
private fun defaultInternalName(location: Location): String {
//no stratum or source path => use default one
val referenceFqName = location.declaringType().name()
// JDI names are of form "package.Class$InnerClass"
return referenceFqName.replace('.', '/')
}
override fun getAllClasses(sourcePosition: SourcePosition): List<ReferenceType> {
val psiFile = sourcePosition.file
if (psiFile is KtFile) {
if (!ProjectRootsUtil.isInProjectOrLibSource(psiFile)) return emptyList()
return DebuggerClassNameProvider(myDebugProcess).getClassesForPosition(sourcePosition)
}
if (psiFile is ClsFileImpl) {
val decompiledPsiFile = psiFile.readAction { it.decompiledPsiFile }
if (decompiledPsiFile is KtClsFile && runReadAction { sourcePosition.line } == -1) {
val className = JvmFileClassUtil.getFileClassInternalName(decompiledPsiFile)
return myDebugProcess.virtualMachineProxy.classesByName(className)
}
}
throw NoDataException.INSTANCE
}
fun originalClassNamesForPosition(position: SourcePosition): List<String> {
return DebuggerClassNameProvider(myDebugProcess, findInlineUseSites = false).getOuterClassNamesForPosition(position)
}
override fun locationsOfLine(type: ReferenceType, position: SourcePosition): List<Location> {
if (position.file !is KtFile) {
throw NoDataException.INSTANCE
}
try {
if (myDebugProcess.isDexDebug()) {
val inlineLocations = runReadAction { getLocationsOfInlinedLine(type, position, myDebugProcess.searchScope) }
if (!inlineLocations.isEmpty()) {
return inlineLocations
}
}
val line = position.line + 1
val locations = type.locationsOfLine(KOTLIN_STRATA_NAME, null, line)
if (locations == null || locations.isEmpty()) {
throw NoDataException.INSTANCE
}
return locations.filter { it.sourceName(KOTLIN_STRATA_NAME) == position.file.name }
}
catch (e: AbsentInformationException) {
throw NoDataException.INSTANCE
}
}
@Deprecated("Since Idea 14.0.3 use createPrepareRequests fun")
override fun createPrepareRequest(classPrepareRequestor: ClassPrepareRequestor, sourcePosition: SourcePosition): ClassPrepareRequest? {
return createPrepareRequests(classPrepareRequestor, sourcePosition).firstOrNull()
}
override fun createPrepareRequests(requestor: ClassPrepareRequestor, position: SourcePosition): List<ClassPrepareRequest> {
if (position.file !is KtFile) {
throw NoDataException.INSTANCE
}
return DumbService.getInstance(myDebugProcess.project).runReadActionInSmartMode(Computable {
val classNames = DebuggerClassNameProvider(myDebugProcess).getOuterClassNamesForPosition(position)
classNames.flatMap { name ->
listOfNotNull(
myDebugProcess.requestsManager.createClassPrepareRequest(requestor, name),
myDebugProcess.requestsManager.createClassPrepareRequest(requestor, "$name$*")
)
}
})
}
}
inline fun <U, V> U.readAction(crossinline f: (U) -> V): V {
return runReadAction { f(this) }
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger;
import com.intellij.debugger.PositionManager;
import com.intellij.debugger.PositionManagerFactory;
import com.intellij.debugger.engine.DebugProcess;
import org.jetbrains.annotations.NotNull;
public class KotlinPositionManagerFactory extends PositionManagerFactory {
@Override
public PositionManager createPositionManager(@NotNull DebugProcess process) {
return new KotlinPositionManager(process);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.SourcePositionHighlighter
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.psi.KtFunctionLiteral
class KotlinSourcePositionHighlighter: SourcePositionHighlighter() {
override fun getHighlightRange(sourcePosition: SourcePosition?): TextRange? {
val lambda = sourcePosition?.elementAt?.parent
if (lambda is KtFunctionLiteral) {
return lambda.textRange
}
return null
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.SourcePositionProvider
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerContextUtil
import com.intellij.debugger.impl.PositionUtil
import com.intellij.debugger.ui.tree.FieldDescriptor
import com.intellij.debugger.ui.tree.LocalVariableDescriptor
import com.intellij.debugger.ui.tree.NodeDescriptor
import com.intellij.openapi.project.Project
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.ClassNotPreparedException
import com.sun.jdi.ReferenceType
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContextUtils
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
import org.jetbrains.kotlin.resolve.source.getPsi
class KotlinSourcePositionProvider : SourcePositionProvider() {
override fun computeSourcePosition(descriptor: NodeDescriptor, project: Project, context: DebuggerContextImpl, nearest: Boolean): SourcePosition? {
if (context.frameProxy == null) return null
if (descriptor is FieldDescriptor) {
return computeSourcePosition(descriptor, project, context, nearest)
}
if (descriptor is LocalVariableDescriptor) {
return computeSourcePosition(descriptor, project, context, nearest)
}
return null
}
private fun computeSourcePosition(
descriptor: LocalVariableDescriptor,
project: Project,
context: DebuggerContextImpl,
nearest: Boolean
): SourcePosition? {
val place = PositionUtil.getContextElement(context) ?: return null
if (place.containingFile !is KtFile) return null
val contextElement = getContextElement(place) ?: return null
val codeFragment = KtPsiFactory(project).createExpressionCodeFragment(descriptor.name, contextElement)
val expression = codeFragment.getContentElement()
if (expression is KtSimpleNameExpression) {
val bindingContext = expression.analyze(BodyResolveMode.PARTIAL)
val declarationDescriptor = BindingContextUtils.extractVariableDescriptorFromReference(bindingContext, expression)
val sourceElement = declarationDescriptor?.source
if (sourceElement is KotlinSourceElement) {
val element = sourceElement.getPsi() ?: return null
if (nearest) {
return DebuggerContextUtil.findNearest(context, element, element.containingFile)
}
return SourcePosition.createFromOffset(element.containingFile, element.textOffset)
}
}
return null
}
private fun computeSourcePosition(descriptor: FieldDescriptor, project: Project, context: DebuggerContextImpl, nearest: Boolean): SourcePosition? {
val fieldName = descriptor.field.name()
if (fieldName == AsmUtil.CAPTURED_THIS_FIELD
|| fieldName == AsmUtil.CAPTURED_RECEIVER_FIELD
|| fieldName.startsWith(AsmUtil.LABELED_THIS_FIELD)
) {
return null
}
val type = descriptor.field.declaringType()
val myClass = findClassByType(project, type, context)?.navigationElement as? KtClassOrObject ?: return null
val field = myClass.declarations.firstOrNull { fieldName == it.name } ?: return null
if (nearest) {
return DebuggerContextUtil.findNearest(context, field, myClass.containingFile)
}
return SourcePosition.createFromOffset(field.containingFile, field.textOffset)
}
private fun findClassByType(project: Project, type: ReferenceType, context: DebuggerContextImpl): PsiElement? {
val session = context.debuggerSession
val scope = session?.searchScope ?: GlobalSearchScope.allScope(project)
val className = JvmClassName.byInternalName(type.name()).fqNameForClassNameWithoutDollars.asString()
val myClass = JavaPsiFacade.getInstance(project).findClass(className, scope)
if (myClass != null) return myClass
val position = getLastSourcePosition(type, context)
if (position != null) {
val element = position.elementAt
if (element != null) {
return element.getStrictParentOfType<KtClassOrObject>()
}
}
return null
}
private fun getLastSourcePosition(type: ReferenceType, context: DebuggerContextImpl): SourcePosition? {
val debugProcess = context.debugProcess
if (debugProcess != null) {
try {
val locations = type.allLineLocations()
if (!locations.isEmpty()) {
val lastLocation = locations.get(locations.size - 1)
return debugProcess.positionManager.getSourcePosition(lastLocation)
}
}
catch (ignored: AbsentInformationException) {
}
catch (ignored: ClassNotPreparedException) {
}
}
return null
}
}
@@ -0,0 +1,416 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.descriptors.data.DescriptorData
import com.intellij.debugger.impl.descriptors.data.DisplayKey
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.debugger.ui.impl.watch.ThisDescriptorImpl
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiExpression
import com.intellij.util.IncorrectOperationException
import com.intellij.xdebugger.frame.XValue
import com.intellij.xdebugger.frame.XValueChildrenList
import com.sun.jdi.ObjectReference
import com.sun.jdi.ReferenceType
import com.sun.jdi.Type
import com.sun.jdi.Value
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.AsmUtil.THIS
import org.jetbrains.kotlin.codegen.DESTRUCTURED_LAMBDA_ARGUMENT_VARIABLE_PREFIX
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.codegen.inline.isFakeLocalVariableForInline
import org.jetbrains.org.objectweb.asm.Type as AsmType
import org.jetbrains.kotlin.utils.getSafe
import java.lang.reflect.Modifier
import java.util.*
import kotlin.coroutines.Continuation
class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDescriptorImpl(frame, MethodsTracker()), true) {
private companion object {
private val LOG = Logger.getInstance(this::class.java)
}
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
override fun superBuildVariables(evaluationContext: EvaluationContextImpl, children: XValueChildrenList) {
if (!kotlinVariableViewService.kotlinVariableView) {
return super.superBuildVariables(evaluationContext, children)
}
val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager
fun addItem(variable: LocalVariableProxyImpl) {
if (nodeManager == null) {
return
}
val variableDescriptor = nodeManager.getLocalVariableDescriptor(null, variable)
children.add(JavaValue.create(null, variableDescriptor, evaluationContext, nodeManager, false))
}
val (thisReferences, otherVariables) = visibleVariables
.partition { it.name() == THIS || it is ThisLocalVariable }
if (!removeSyntheticThisObject(evaluationContext, children, thisReferences) && thisReferences.isNotEmpty()) {
val thisLabels = thisReferences.asSequence()
.filterIsInstance<ThisLocalVariable>()
.mapNotNullTo(hashSetOf()) { it.label }
remapThisObjectForOuterThis(evaluationContext, children, thisLabels)
}
thisReferences.forEach(::addItem)
otherVariables.forEach(::addItem)
}
private fun removeSyntheticThisObject(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
thisReferences: List<LocalVariableProxyImpl>
): Boolean {
val thisObject = evaluationContext.frameProxy?.thisObject() ?: return false
if (thisObject.type().isSubtype(CONTINUATION_TYPE)) {
ExistingInstanceThis.find(children)?.remove()
return true
}
val thisObjectType = thisObject.type()
if (thisObjectType.isSubtype(Function::class.java.name) && '$' in thisObjectType.signature()) {
val existingThis = ExistingInstanceThis.find(children)
if (existingThis != null) {
existingThis.remove()
val javaValue = existingThis.value as? JavaValue
if (javaValue != null) {
attachCapturedThisFromLambda(evaluationContext, children, javaValue, thisReferences)
}
}
return true
}
return removeCallSiteThisInInlineFunction(evaluationContext, children)
}
private fun removeCallSiteThisInInlineFunction(evaluationContext: EvaluationContextImpl, children: XValueChildrenList): Boolean {
val frameProxy = evaluationContext.frameProxy
val variables = frameProxy?.safeVisibleVariables() ?: return false
val inlineDepth = getInlineDepth(variables)
val declarationSiteThis = variables.firstOrNull { v ->
val name = v.name()
name.endsWith(INLINE_FUN_VAR_SUFFIX) && name.dropInlineSuffix() == AsmUtil.INLINE_DECLARATION_SITE_THIS
}
if (inlineDepth > 0 && declarationSiteThis != null) {
ExistingInstanceThis.find(children)?.remove()
return true
}
return false
}
private fun attachCapturedThisFromLambda(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
javaValue: JavaValue,
thisReferences: List<LocalVariableProxyImpl>
) {
try {
val value = javaValue.descriptor.calcValue(evaluationContext) as? ObjectReference ?: return
val thisField = value.referenceType().fieldByName(AsmUtil.CAPTURED_THIS_FIELD) ?: return
val thisValue = value.getValue(thisField) as? ObjectReference ?: return
val thisType = thisValue.referenceType()
val unsafeLabel = generateThisLabelUnsafe(thisType) ?: return
val label = checkLabel(unsafeLabel)
if (label != null) {
val thisName = getThisName(label)
if (thisReferences.any { it.name() == thisName }) {
// Avoid label duplication
return
}
}
val thisName = when {
thisReferences.isEmpty() -> THIS
label != null -> getThisName(label)
else -> "$THIS (anonymous fun)"
}
val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager ?: return
val thisDescriptor = nodeManager.getDescriptor(this.descriptor, LabeledThisData(thisName, thisValue))
children.add(JavaValue.create(null, thisDescriptor, evaluationContext, nodeManager, false))
} catch (e: EvaluateException) {
// do nothing
}
}
private fun remapThisObjectForOuterThis(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
existingThisLabels: Set<String>
) {
val thisObject = evaluationContext.frameProxy?.thisObject() ?: return
val variable = ExistingInstanceThis.find(children) ?: return
val thisLabel = generateThisLabel(thisObject.referenceType())?.takeIf { it !in existingThisLabels }
if (thisLabel == null) {
variable.remove()
return
}
// add additional checks?
variable.remapName(getThisName(thisLabel))
}
// Very Dirty Work-around.
// Hopefully, there will be an API for that in 2019.1.
private class ExistingInstanceThis(
private val children: XValueChildrenList,
private val index: Int,
val value: XValue,
private val size: Int
) {
companion object {
private const val THIS_NAME = "this"
fun find(children: XValueChildrenList): ExistingInstanceThis? {
val size = children.size()
for (i in 0 until size) {
if (children.getName(i) == THIS_NAME) {
val valueDescriptor = (children.getValue(i) as? JavaValue)?.descriptor
@Suppress("FoldInitializerAndIfToElvis")
if (valueDescriptor !is ThisDescriptorImpl) {
return null
}
return ExistingInstanceThis(children, i, children.getValue(i), size)
}
}
return null
}
}
fun remapName(newName: String) {
val (names, _) = getLists() ?: return
names[index] = newName
}
fun remove() {
val (names, values) = getLists() ?: return
names.removeAt(index)
values.removeAt(index)
}
private fun getLists(): Lists? {
if (children.size() != size) {
throw IllegalStateException("Children list was modified")
}
var namesList: MutableList<Any?>? = null
var valuesList: MutableList<Any?>? = null
for (field in XValueChildrenList::class.java.declaredFields) {
val mods = field.modifiers
if (Modifier.isPrivate(mods) && Modifier.isFinal(mods) && !Modifier.isStatic(mods) && field.type == List::class.java) {
@Suppress("UNCHECKED_CAST")
val list = (field.getSafe(children) as? MutableList<Any?>)?.takeIf { it.size == size } ?: continue
if (list[index] == THIS_NAME) {
namesList = list
} else if (list[index] === value) {
valuesList = list
}
}
if (namesList != null && valuesList != null) {
return Lists(namesList, valuesList)
}
}
LOG.error(
"Can't find name/value lists, existing fields: "
+ Arrays.toString(XValueChildrenList::class.java.declaredFields)
)
return null
}
private data class Lists(val names: MutableList<Any?>, val values: MutableList<Any?>)
}
override fun getVisibleVariables(): List<LocalVariableProxyImpl> {
val allVisibleVariables = super.getStackFrameProxy().safeVisibleVariables()
if (!kotlinVariableViewService.kotlinVariableView) {
return allVisibleVariables.map { variable ->
if (isFakeLocalVariableForInline(variable.name())) variable.wrapSyntheticInlineVariable() else variable
}
}
val inlineDepth = getInlineDepth(allVisibleVariables)
val (thisVariables, otherVariables) = allVisibleVariables.asSequence()
.filter { !isHidden(it, inlineDepth) }
.partition {
it.name() == THIS
|| it.name() == AsmUtil.THIS_IN_DEFAULT_IMPLS
|| it.name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
|| (INLINED_THIS_REGEX.matches(it.name()))
}
val (mainThis, otherThis) = thisVariables
.sortedByDescending { it.variable }
.let { it.firstOrNull() to it.drop(1) }
val remappedMainThis = mainThis?.clone(THIS, null)
val remappedOther = (otherThis + otherVariables).map { it.remapVariableNameIfNeeded() }
return (listOfNotNull(remappedMainThis) + remappedOther).sortedBy { it.variable }
}
private fun isHidden(variable: LocalVariableProxyImpl, inlineDepth: Int): Boolean {
val name = variable.name()
return isFakeLocalVariableForInline(name)
|| name.startsWith(DESTRUCTURED_LAMBDA_ARGUMENT_VARIABLE_PREFIX)
|| name.startsWith(AsmUtil.LOCAL_FUNCTION_VARIABLE_PREFIX)
|| getInlineDepth(variable.name()) != inlineDepth
|| name == CONTINUATION_VARIABLE_NAME
}
private fun LocalVariableProxyImpl.remapVariableNameIfNeeded(): LocalVariableProxyImpl {
val name = this.name().dropInlineSuffix()
@Suppress("ConvertToStringTemplate")
return when {
isLabeledThisReference() -> {
val label = name.drop(AsmUtil.LABELED_THIS_PARAMETER.length)
clone(getThisName(label), label)
}
name == AsmUtil.THIS_IN_DEFAULT_IMPLS -> clone(THIS + " (outer)", null)
name == AsmUtil.RECEIVER_PARAMETER_NAME -> clone(THIS + " (receiver)", null)
INLINED_THIS_REGEX.matches(name) -> {
val label = generateThisLabel(frame.getValue(this)?.type())
if (label != null) {
clone(getThisName(label), label)
} else {
this@remapVariableNameIfNeeded
}
}
name != this.name() -> {
object : LocalVariableProxyImpl(frame, variable) {
override fun name() = name
}
}
else -> this@remapVariableNameIfNeeded
}
}
private fun generateThisLabel(type: Type?): String? {
return checkLabel(generateThisLabelUnsafe(type) ?: return null)
}
private fun generateThisLabelUnsafe(type: Type?): String? {
val referenceType = type as? ReferenceType ?: return null
return referenceType.name().substringAfterLast('.').substringAfterLast('$')
}
private fun checkLabel(label: String): String? {
if (label.isEmpty() || label.all { it.isDigit() }) {
return null
}
return label
}
private fun String.dropInlineSuffix(): String {
val depth = getInlineDepth(this)
if (depth == 0) {
return this
}
return dropLast(depth * INLINE_FUN_VAR_SUFFIX.length)
}
private fun LocalVariableProxyImpl.clone(name: String, label: String?): LocalVariableProxyImpl {
return object : LocalVariableProxyImpl(frame, variable), ThisLocalVariable {
override fun name() = name
override val label = label
}
}
private fun LocalVariableProxyImpl.isLabeledThisReference(): Boolean {
@Suppress("ConvertToStringTemplate")
return name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
}
}
private interface ThisLocalVariable {
val label: String?
}
private fun LocalVariableProxyImpl.wrapSyntheticInlineVariable(): LocalVariableProxyImpl {
val proxyWrapper = object : StackFrameProxyImpl(frame.threadProxy(), frame.stackFrame, frame.indexFromBottom) {
override fun getValue(localVariable: LocalVariableProxyImpl): Value {
return frame.virtualMachine.mirrorOfVoid()
}
}
return LocalVariableProxyImpl(proxyWrapper, variable)
}
private fun getThisName(label: String): String {
return "$THIS (@$label)"
}
private class LabeledThisData(val name: String, val value: ObjectReference) : DescriptorData<ValueDescriptorImpl>() {
override fun createDescriptorImpl(project: Project): ValueDescriptorImpl {
return object : ValueDescriptorImpl(project, value) {
override fun getName() = this@LabeledThisData.name
override fun calcValue(evaluationContext: EvaluationContextImpl?) = value
override fun canSetValue() = false
override fun getDescriptorEvaluation(context: DebuggerContext?): PsiExpression {
// TODO change to labeled this
val elementFactory = JavaPsiFacade.getElementFactory(myProject)
try {
return elementFactory.createExpressionFromText("this", null)
} catch (e: IncorrectOperationException) {
throw EvaluateException(e.message, e)
}
}
}
}
override fun getDisplayKey(): DisplayKey<ValueDescriptorImpl> = SimpleDisplayKey(this)
override fun equals(other: Any?) = other is LabeledThisData && other.name == name
override fun hashCode() = name.hashCode()
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.components.ServiceManager
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.impl.XDebuggerUtilImpl
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
class ToggleKotlinVariablesState {
companion object {
private const val KOTLIN_VARIABLE_VIEW = "debugger.kotlin.variable.view"
fun getService(): ToggleKotlinVariablesState {
return ServiceManager.getService(ToggleKotlinVariablesState::class.java)
}
}
var kotlinVariableView = PropertiesComponent.getInstance().getBoolean(KOTLIN_VARIABLE_VIEW, true)
set(newValue) {
field = newValue
PropertiesComponent.getInstance().setValue(KOTLIN_VARIABLE_VIEW, newValue)
}
}
class ToggleKotlinVariablesView : ToggleAction() {
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
override fun update(e: AnActionEvent) {
super.update(e)
val session = XDebugSession.DATA_KEY.getData(e.dataContext)
e.presentation.isEnabledAndVisible = session != null && session.isInKotlinFile()
}
private fun XDebugSession.isInKotlinFile(): Boolean {
val fileExtension = currentPosition?.file?.extension ?: return false
return fileExtension in KotlinFileTypeFactory.KOTLIN_EXTENSIONS
}
override fun isSelected(e: AnActionEvent) = kotlinVariableViewService.kotlinVariableView
override fun setSelected(e: AnActionEvent, state: Boolean) {
kotlinVariableViewService.kotlinVariableView = state
XDebuggerUtilImpl.rebuildAllSessionsViews(e.project)
}
}
@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinBreakpointFiltersPanel">
<grid id="27dc6" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="5736" binding="myConditionsPanel" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="7">
<margin top="2" left="2" bottom="5" right="5"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<clientProperties>
<BorderFactoryClass class="java.lang.String" value="com.intellij.ui.IdeBorderFactory$PlainSmallWithoutIndent"/>
</clientProperties>
<border type="etched" title-resource-bundle="messages/DebuggerBundle" title-key="label.breakpoint.properties.panel.group.conditions"/>
<children>
<grid id="8e867" binding="myInstanceFiltersPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="0">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="17e11" class="javax.swing.JCheckBox" binding="myInstanceFiltersCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<margin top="2" left="2" bottom="0" right="2"/>
<text resource-bundle="messages/DebuggerBundle" key="breakpoint.properties.panel.option.instance.filters"/>
</properties>
</component>
<grid id="5231f" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<hspacer id="eeee7">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
</hspacer>
<xy id="28068" binding="myInstanceFiltersFieldPanel" layout-manager="XYLayout" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</xy>
</children>
</grid>
</children>
</grid>
<grid id="25884" binding="myClassFiltersPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="0">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="729d5" class="javax.swing.JCheckBox" binding="myClassFiltersCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<margin top="2" left="2" bottom="0" right="2"/>
<text resource-bundle="messages/DebuggerBundle" key="breakpoint.properties.panel.option.class.filters"/>
</properties>
</component>
<grid id="9bef6" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<xy id="e3d10" binding="myClassFiltersFieldPanel" layout-manager="XYLayout" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</xy>
<hspacer id="ec2a">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
</hspacer>
</children>
</grid>
</children>
</grid>
<grid id="275ca" binding="myPassCountPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="0">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="7" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="27bbc" class="javax.swing.JCheckBox" binding="myPassCountCheckbox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<margin top="2" left="2" bottom="0" right="2"/>
<text resource-bundle="messages/DebuggerBundle" key="breakpoint.properties.panel.option.pass.count"/>
</properties>
</component>
<grid id="71095" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="33fef" class="javax.swing.JTextField" binding="myPassCountField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
<properties>
<enabled value="false"/>
<horizontalAlignment value="10"/>
</properties>
</component>
<hspacer id="28e6f">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
</hspacer>
</children>
</grid>
</children>
</grid>
<vspacer id="d1c29">
<constraints>
<grid row="3" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</children>
</grid>
</form>
@@ -0,0 +1,403 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints;
import com.intellij.debugger.InstanceFilter;
import com.intellij.debugger.ui.breakpoints.EditClassFiltersDialog;
import com.intellij.debugger.ui.breakpoints.EditInstanceFiltersDialog;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiClass;
import com.intellij.ui.FieldPanel;
import com.intellij.ui.MultiLineTooltipUI;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel;
import com.intellij.xdebugger.impl.breakpoints.XBreakpointBase;
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.java.debugger.breakpoints.properties.JavaBreakpointProperties;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class KotlinBreakpointFiltersPanel<T extends KotlinPropertyBreakpointProperties, B extends XBreakpoint<T>> extends XBreakpointCustomPropertiesPanel<B> {
private JPanel myConditionsPanel;
private JPanel myInstanceFiltersPanel;
private JCheckBox myInstanceFiltersCheckBox;
private JPanel myInstanceFiltersFieldPanel;
private JPanel myClassFiltersPanel;
private JCheckBox myClassFiltersCheckBox;
private JPanel myClassFiltersFieldPanel;
private JPanel myPassCountPanel;
private JCheckBox myPassCountCheckbox;
private JTextField myPassCountField;
private final FieldPanel myInstanceFiltersField;
private final FieldPanel myClassFiltersField;
private ClassFilter[] myClassFilters = ClassFilter.EMPTY_ARRAY;
private ClassFilter[] myClassExclusionFilters = ClassFilter.EMPTY_ARRAY;
private InstanceFilter[] myInstanceFilters = InstanceFilter.EMPTY_ARRAY;
protected final Project myProject;
private PsiClass myBreakpointPsiClass;
public KotlinBreakpointFiltersPanel(Project project) {
myProject = project;
myInstanceFiltersField = new FieldPanel(new MyTextField(), "", null,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
reloadInstanceFilters();
EditInstanceFiltersDialog _dialog = new EditInstanceFiltersDialog(myProject);
_dialog.setFilters(myInstanceFilters);
_dialog.show();
if (_dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
myInstanceFilters = _dialog.getFilters();
updateInstanceFilterEditor(true);
}
}
},
null
);
myClassFiltersField = new FieldPanel(new MyTextField(), "", null,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
reloadClassFilters();
com.intellij.ide.util.ClassFilter classFilter = createClassConditionFilter();
EditClassFiltersDialog _dialog = new EditClassFiltersDialog(myProject, classFilter);
_dialog.setFilters(myClassFilters, myClassExclusionFilters);
_dialog.show();
if (_dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
myClassFilters = _dialog.getFilters();
myClassExclusionFilters = _dialog.getExclusionFilters();
updateClassFilterEditor(true);
}
}
},
null
);
ActionListener updateListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateCheckboxes();
}
};
myPassCountCheckbox.addActionListener(updateListener);
myInstanceFiltersCheckBox.addActionListener(updateListener);
myClassFiltersCheckBox.addActionListener(updateListener);
ToolTipManager.sharedInstance().registerComponent(myClassFiltersField.getTextField());
ToolTipManager.sharedInstance().registerComponent(myInstanceFiltersField.getTextField());
insert(myInstanceFiltersFieldPanel, myInstanceFiltersField);
insert(myClassFiltersFieldPanel, myClassFiltersField);
DebuggerUIUtil.focusEditorOnCheck(myPassCountCheckbox, myPassCountField);
DebuggerUIUtil.focusEditorOnCheck(myInstanceFiltersCheckBox, myInstanceFiltersField.getTextField());
DebuggerUIUtil.focusEditorOnCheck(myClassFiltersCheckBox, myClassFiltersField.getTextField());
}
@NotNull
@Override
public JComponent getComponent() {
return myConditionsPanel;
}
@Override
public boolean isVisibleOnPopup(@NotNull B breakpoint) {
JavaBreakpointProperties properties = breakpoint.getProperties();
if (properties != null) {
return properties.isCOUNT_FILTER_ENABLED() || properties.isCLASS_FILTERS_ENABLED() || properties.isINSTANCE_FILTERS_ENABLED();
}
return false;
}
@Override
public void saveTo(@NotNull B breakpoint) {
JavaBreakpointProperties properties = breakpoint.getProperties();
if (properties == null) {
return;
}
boolean changed = false;
try {
String text = myPassCountField.getText().trim();
int filter = !text.isEmpty() ? Integer.parseInt(text) : 0;
if (filter < 0) filter = 0;
changed = properties.setCOUNT_FILTER(filter);
}
catch (Exception ignored) {
}
changed = properties.setCOUNT_FILTER_ENABLED(properties.getCOUNT_FILTER() > 0 && myPassCountCheckbox.isSelected()) || changed;
reloadInstanceFilters();
reloadClassFilters();
updateInstanceFilterEditor(true);
updateClassFilterEditor(true);
changed = properties.setINSTANCE_FILTERS_ENABLED(myInstanceFiltersField.getText().length() > 0 && myInstanceFiltersCheckBox.isSelected()) || changed;
changed = properties.setCLASS_FILTERS_ENABLED(myClassFiltersField.getText().length() > 0 && myClassFiltersCheckBox.isSelected()) || changed;
changed = properties.setClassFilters(myClassFilters) || changed;
changed = properties.setClassExclusionFilters(myClassExclusionFilters) || changed;
changed = properties.setInstanceFilters(myInstanceFilters) || changed;
if (changed) {
((XBreakpointBase)breakpoint).fireBreakpointChanged();
}
}
private static void insert(JPanel panel, JComponent component) {
panel.setLayout(new BorderLayout());
panel.add(component, BorderLayout.CENTER);
}
@Override
public void loadFrom(@NotNull B breakpoint) {
JavaBreakpointProperties properties = breakpoint.getProperties();
if (properties != null) {
if (properties.getCOUNT_FILTER() > 0) {
myPassCountField.setText(Integer.toString(properties.getCOUNT_FILTER()));
}
else {
myPassCountField.setText("");
}
myPassCountCheckbox.setSelected(properties.isCOUNT_FILTER_ENABLED());
myInstanceFiltersCheckBox.setSelected(properties.isINSTANCE_FILTERS_ENABLED());
myInstanceFiltersField.setEnabled(properties.isINSTANCE_FILTERS_ENABLED());
myInstanceFiltersField.getTextField().setEditable(properties.isINSTANCE_FILTERS_ENABLED());
myInstanceFilters = properties.getInstanceFilters();
updateInstanceFilterEditor(true);
myClassFiltersCheckBox.setSelected(properties.isCLASS_FILTERS_ENABLED());
myClassFiltersField.setEnabled(properties.isCLASS_FILTERS_ENABLED());
myClassFiltersField.getTextField().setEditable(properties.isCLASS_FILTERS_ENABLED());
myClassFilters = properties.getClassFilters();
myClassExclusionFilters = properties.getClassExclusionFilters();
updateClassFilterEditor(true);
XSourcePosition position = breakpoint.getSourcePosition();
// TODO: need to calculate psi class
//myBreakpointPsiClass = breakpoint.getPsiClass();
}
updateCheckboxes();
}
private void updateInstanceFilterEditor(boolean updateText) {
List<String> filters = new ArrayList<String>();
for (InstanceFilter instanceFilter : myInstanceFilters) {
if (instanceFilter.isEnabled()) {
filters.add(Long.toString(instanceFilter.getId()));
}
}
if (updateText) {
myInstanceFiltersField.setText(StringUtil.join(filters, " "));
}
String tipText = concatWithEx(filters, " ", (int)Math.sqrt(myInstanceFilters.length) + 1, "\n");
myInstanceFiltersField.getTextField().setToolTipText(tipText);
}
private class MyTextField extends JTextField {
public MyTextField() {
}
@Override
public String getToolTipText(MouseEvent event) {
reloadClassFilters();
updateClassFilterEditor(false);
reloadInstanceFilters();
updateInstanceFilterEditor(false);
String toolTipText = super.getToolTipText(event);
return getToolTipText().length() == 0 ? null : toolTipText;
}
@Override
public JToolTip createToolTip() {
JToolTip toolTip = new JToolTip(){{
setUI(new MultiLineTooltipUI());
}};
toolTip.setComponent(this);
return toolTip;
}
}
private void reloadClassFilters() {
String filtersText = myClassFiltersField.getText();
ArrayList<ClassFilter> classFilters = new ArrayList<ClassFilter>();
ArrayList<ClassFilter> exclusionFilters = new ArrayList<ClassFilter>();
int startFilter = -1;
for(int i = 0; i <= filtersText.length(); i++) {
if(i < filtersText.length() && !Character.isWhitespace(filtersText.charAt(i))){
if(startFilter == -1) {
startFilter = i;
}
}
else {
if(startFilter >=0) {
if(filtersText.charAt(startFilter) == '-') {
exclusionFilters.add(new ClassFilter(filtersText.substring(startFilter + 1, i)));
}
else {
classFilters.add(new ClassFilter(filtersText.substring(startFilter, i)));
}
startFilter = -1;
}
}
}
for (ClassFilter classFilter : myClassFilters) {
if (!classFilter.isEnabled()) {
classFilters.add(classFilter);
}
}
for (ClassFilter classFilter : myClassExclusionFilters) {
if (!classFilter.isEnabled()) {
exclusionFilters.add(classFilter);
}
}
myClassFilters = classFilters .toArray(new ClassFilter[classFilters .size()]);
myClassExclusionFilters = exclusionFilters.toArray(new ClassFilter[exclusionFilters.size()]);
}
private void reloadInstanceFilters() {
String filtersText = myInstanceFiltersField.getText();
ArrayList<InstanceFilter> idxs = new ArrayList<InstanceFilter>();
int startNumber = -1;
for(int i = 0; i <= filtersText.length(); i++) {
if(i < filtersText.length() && Character.isDigit(filtersText.charAt(i))) {
if(startNumber == -1) {
startNumber = i;
}
}
else {
if(startNumber >=0) {
idxs.add(InstanceFilter.create(filtersText.substring(startNumber, i)));
startNumber = -1;
}
}
}
for (InstanceFilter instanceFilter : myInstanceFilters) {
if (!instanceFilter.isEnabled()) {
idxs.add(instanceFilter);
}
}
myInstanceFilters = idxs.toArray(new InstanceFilter[idxs.size()]);
}
private void updateClassFilterEditor(boolean updateText) {
List<String> filters = new ArrayList<String>();
for (ClassFilter classFilter : myClassFilters) {
if (classFilter.isEnabled()) {
filters.add(classFilter.getPattern());
}
}
List<String> excludeFilters = new ArrayList<String>();
for (ClassFilter classFilter : myClassExclusionFilters) {
if (classFilter.isEnabled()) {
excludeFilters.add("-" + classFilter.getPattern());
}
}
if (updateText) {
String editorText = StringUtil.join(filters, " ");
if(!filters.isEmpty()) {
editorText += " ";
}
editorText += StringUtil.join(excludeFilters, " ");
myClassFiltersField.setText(editorText);
}
int width = (int)Math.sqrt(myClassExclusionFilters.length + myClassFilters.length) + 1;
String tipText = concatWithEx(filters, " ", width, "\n");
if(!filters.isEmpty()) {
tipText += "\n";
}
tipText += concatWithEx(excludeFilters, " ", width, "\n");
myClassFiltersField.getTextField().setToolTipText(tipText);
}
private static String concatWithEx(List<String> s, String concator, int N, String NthConcator) {
String result = "";
int i = 1;
for (Iterator iterator = s.iterator(); iterator.hasNext(); i++) {
String str = (String) iterator.next();
result += str;
if(iterator.hasNext()){
if(i % N == 0){
result += NthConcator;
}
else {
result += concator;
}
}
}
return result;
}
protected com.intellij.ide.util.ClassFilter createClassConditionFilter() {
com.intellij.ide.util.ClassFilter classFilter;
if(myBreakpointPsiClass != null) {
classFilter = new com.intellij.ide.util.ClassFilter() {
@Override
public boolean isAccepted(PsiClass aClass) {
return myBreakpointPsiClass == aClass || aClass.isInheritor(myBreakpointPsiClass, true);
}
};
}
else {
classFilter = null;
}
return classFilter;
}
protected void updateCheckboxes() {
boolean passCountApplicable = true;
if (myInstanceFiltersCheckBox.isSelected() || myClassFiltersCheckBox.isSelected()) {
passCountApplicable = false;
}
myPassCountCheckbox.setEnabled(passCountApplicable);
boolean passCountSelected = myPassCountCheckbox.isSelected();
myInstanceFiltersCheckBox.setEnabled(!passCountSelected);
myClassFiltersCheckBox.setEnabled(!passCountSelected);
myPassCountField.setEditable(myPassCountCheckbox.isSelected());
myPassCountField.setEnabled (myPassCountCheckbox.isSelected());
myInstanceFiltersField.setEnabled(myInstanceFiltersCheckBox.isSelected());
myInstanceFiltersField.getTextField().setEditable(myInstanceFiltersCheckBox.isSelected());
myClassFiltersField.setEnabled(myClassFiltersCheckBox.isSelected());
myClassFiltersField.getTextField().setEditable(myClassFiltersCheckBox.isSelected());
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaBreakpointHandler
import com.intellij.debugger.engine.JavaBreakpointHandlerFactory
class KotlinFieldBreakpointHandlerFactory : JavaBreakpointHandlerFactory {
override fun createHandler(process: DebugProcessImpl): JavaBreakpointHandler? {
return KotlinFieldBreakpointHandler(process)
}
}
class KotlinLineBreakpointHandlerFactory: JavaBreakpointHandlerFactory {
override fun createHandler(process: DebugProcessImpl): JavaBreakpointHandler? {
return KotlinLineBreakpointHandler(process)
}
}
class KotlinFieldBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinFieldBreakpointType::class.java, process)
class KotlinLineBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinLineBreakpointType::class.java, process)
@@ -0,0 +1,397 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.PositionUtil
import com.intellij.debugger.requests.Requestor
import com.intellij.debugger.ui.breakpoints.BreakpointCategory
import com.intellij.debugger.ui.breakpoints.BreakpointWithHighlighter
import com.intellij.debugger.ui.breakpoints.FieldBreakpoint
import com.intellij.icons.AllIcons
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.xdebugger.breakpoints.XBreakpoint
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.Method
import com.sun.jdi.ReferenceType
import com.sun.jdi.event.*
import com.sun.jdi.request.EventRequest
import com.sun.jdi.request.MethodEntryRequest
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.resolve.BindingContext
import javax.swing.Icon
class KotlinFieldBreakpoint(
project: Project,
breakpoint: XBreakpoint<KotlinPropertyBreakpointProperties>
): BreakpointWithHighlighter<KotlinPropertyBreakpointProperties>(project, breakpoint) {
companion object {
private val LOG = Logger.getInstance("#org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinFieldBreakpoint")
private val CATEGORY: Key<FieldBreakpoint> = BreakpointCategory.lookup<FieldBreakpoint>("field_breakpoints")
}
private enum class BreakpointType {
FIELD,
METHOD
}
private var breakpointType: BreakpointType = BreakpointType.FIELD
override fun isValid(): Boolean {
if (!BreakpointWithHighlighter.isPositionValid(xBreakpoint.sourcePosition)) return false
return runReadAction {
val field = getField()
field != null && field.isValid
}
}
fun getField(): KtCallableDeclaration? {
val sourcePosition = sourcePosition
return getProperty(sourcePosition)
}
private fun getProperty(sourcePosition: SourcePosition?): KtCallableDeclaration? {
val property: KtProperty? = PositionUtil.getPsiElementAt(project, KtProperty::class.java, sourcePosition)
if (property != null) {
return property
}
val parameter: KtParameter? = PositionUtil.getPsiElementAt(project, KtParameter::class.java, sourcePosition)
if (parameter != null) {
return parameter
}
return null
}
override fun reload() {
super.reload()
val property = getProperty(sourcePosition) ?: return
val propertyName = property.name ?: return
setFieldName(propertyName)
if (property is KtProperty && property.isTopLevel) {
properties.myClassName = JvmFileClassUtil.getFileClassInfoNoResolve(property.getContainingKtFile()).fileClassFqName.asString()
} else {
val ktClass: KtClassOrObject? = PsiTreeUtil.getParentOfType(property, KtClassOrObject::class.java)
if (ktClass is KtClassOrObject) {
val fqName = ktClass.fqName
if (fqName != null) {
properties.myClassName = fqName.asString()
}
}
}
isInstanceFiltersEnabled = false
}
override fun createRequestForPreparedClass(debugProcess: DebugProcessImpl?, refType: ReferenceType?) {
if (debugProcess == null || refType == null) return
val property = getProperty(sourcePosition) ?: return
breakpointType = (computeBreakpointType(property) ?: return)
val vm = debugProcess.virtualMachineProxy
try {
if (properties.WATCH_INITIALIZATION) {
val sourcePosition = sourcePosition
if (sourcePosition != null) {
debugProcess.positionManager
.locationsOfLine(refType, sourcePosition)
.filter { it.method().isConstructor || it.method().isStaticInitializer }
.forEach {
val request = debugProcess.requestsManager.createBreakpointRequest(this, it)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Breakpoint request added")
}
}
}
}
when (breakpointType) {
BreakpointType.FIELD -> {
val field = refType.fieldByName(getFieldName())
if (field != null) {
val manager = debugProcess.requestsManager
if (properties.WATCH_MODIFICATION && vm.canWatchFieldModification()) {
val request = manager.createModificationWatchpointRequest(this, field)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Modification request added")
}
}
if (properties.WATCH_ACCESS && vm.canWatchFieldAccess()) {
val request = manager.createAccessWatchpointRequest(this, field)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Field access request added (field = ${field.name()}; refType = ${refType.name()})")
}
}
}
}
BreakpointType.METHOD -> {
val fieldName = getFieldName()
if (properties.WATCH_ACCESS) {
val getter = refType.methodsByName(JvmAbi.getterName(fieldName)).firstOrNull()
if (getter != null) {
createMethodBreakpoint(debugProcess, refType, getter)
}
}
if (properties.WATCH_MODIFICATION) {
val setter = refType.methodsByName(JvmAbi.setterName(fieldName)).firstOrNull()
if (setter != null) {
createMethodBreakpoint(debugProcess, refType, setter)
}
}
}
}
}
catch (ex: Exception) {
LOG.debug(ex)
}
}
private fun computeBreakpointType(property: KtCallableDeclaration): BreakpointType? {
return runReadAction {
val bindingContext = property.analyze()
var descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, property)
if (descriptor is ValueParameterDescriptor) {
descriptor = bindingContext.get(BindingContext.VALUE_PARAMETER_AS_PROPERTY, descriptor)
}
if (descriptor is PropertyDescriptor) {
if (bindingContext.get(BindingContext.BACKING_FIELD_REQUIRED, descriptor)!!) {
BreakpointType.FIELD
}
else {
BreakpointType.METHOD
}
}
else {
null
}
}
}
private fun createMethodBreakpoint(debugProcess: DebugProcessImpl, refType: ReferenceType, accessor: Method) {
val manager = debugProcess.requestsManager
val line = accessor.safeAllLineLocations().firstOrNull()
if (line != null) {
val request = manager.createBreakpointRequest(this, line)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Breakpoint request added")
}
}
else {
var entryRequest: MethodEntryRequest? = findRequest(debugProcess, MethodEntryRequest::class.java, this)
if (entryRequest == null) {
entryRequest = manager.createMethodEntryRequest(this)!!
if (LOG.isDebugEnabled) {
LOG.debug("Method entry request added (method = ${accessor.name()}; refType = ${refType.name()})")
}
}
else {
entryRequest.disable()
}
entryRequest.addClassFilter(refType)
manager.enableRequest(entryRequest)
}
}
inline private fun <reified T : EventRequest> findRequest(debugProcess: DebugProcessImpl, requestClass: Class<T>, requestor: Requestor): T? {
val requests = debugProcess.requestsManager.findRequests(requestor)
for (eventRequest in requests) {
if (eventRequest::class.java == requestClass) {
return eventRequest as T
}
}
return null
}
override fun evaluateCondition(context: EvaluationContextImpl, event: LocatableEvent): Boolean {
if (breakpointType == BreakpointType.METHOD && !matchesEvent(event)) {
return false
}
return super.evaluateCondition(context, event)
}
fun matchesEvent(event: LocatableEvent): Boolean {
val method = event.location()?.method()
// TODO check property type
return method != null && method.name() in getMethodsName()
}
private fun getMethodsName(): List<String> {
val fieldName = getFieldName()
return listOf(JvmAbi.getterName(fieldName), JvmAbi.setterName(fieldName))
}
override fun getEventMessage(event: LocatableEvent): String {
val location = event.location()!!
val locationQName = location.declaringType().name() + "." + location.method().name()
val locationFileName = try {
location.sourceName()
}
catch (e: AbsentInformationException) {
fileName
}
catch (e: InternalError) {
fileName
}
val locationLine = location.lineNumber()
when (event) {
is ModificationWatchpointEvent-> {
val field = event.field()
return DebuggerBundle.message(
"status.static.field.watchpoint.reached.access",
field.declaringType().name(),
field.name(),
locationQName,
locationFileName,
locationLine)
}
is AccessWatchpointEvent -> {
val field = event.field()
return DebuggerBundle.message(
"status.static.field.watchpoint.reached.access",
field.declaringType().name(),
field.name(),
locationQName,
locationFileName,
locationLine)
}
is MethodEntryEvent -> {
val method = event.method()
return DebuggerBundle.message(
"status.method.entry.breakpoint.reached",
method.declaringType().name() + "." + method.name() + "()",
locationQName,
locationFileName,
locationLine)
}
is MethodExitEvent -> {
val method = event.method()
return DebuggerBundle.message(
"status.method.exit.breakpoint.reached",
method.declaringType().name() + "." + method.name() + "()",
locationQName,
locationFileName,
locationLine)
}
}
return DebuggerBundle.message(
"status.line.breakpoint.reached",
locationQName,
locationFileName,
locationLine)
}
fun setFieldName(fieldName: String) {
properties.myFieldName = fieldName
}
@TestOnly
fun setWatchAccess(value: Boolean) {
properties.WATCH_ACCESS = value
}
@TestOnly
fun setWatchModification(value: Boolean) {
properties.WATCH_MODIFICATION = value
}
@TestOnly
fun setWatchInitialization(value: Boolean) {
properties.WATCH_INITIALIZATION = value
}
override fun getDisabledIcon(isMuted: Boolean): Icon {
val master = DebuggerManagerEx.getInstanceEx(myProject).breakpointManager.findMasterBreakpoint(this)
return when {
isMuted && master == null -> AllIcons.Debugger.Db_muted_disabled_field_breakpoint
isMuted && master != null -> AllIcons.Debugger.Db_muted_dep_field_breakpoint
master != null -> AllIcons.Debugger.Db_dep_field_breakpoint
else -> AllIcons.Debugger.Db_disabled_field_breakpoint
}
}
override fun getSetIcon(isMuted: Boolean): Icon {
return when {
isMuted -> AllIcons.Debugger.Db_muted_field_breakpoint
else -> AllIcons.Debugger.Db_field_breakpoint
}
}
// BUNCH: 182
override fun getInvalidIcon(isMuted: Boolean): Icon {
return AllIcons.Debugger.Db_invalid_breakpoint
}
override fun getVerifiedIcon(isMuted: Boolean): Icon {
return when {
isMuted -> AllIcons.Debugger.Db_muted_field_breakpoint
else -> AllIcons.Debugger.Db_verified_field_breakpoint
}
}
override fun getVerifiedWarningsIcon(isMuted: Boolean): Icon = AllIcons.Debugger.Db_exception_breakpoint
override fun getCategory() = CATEGORY
override fun getDisplayName(): String? {
if (!isValid) {
return DebuggerBundle.message("status.breakpoint.invalid")
}
val className = className
return if (className != null && !className.isEmpty()) className + "." + getFieldName() else getFieldName()
}
private fun getFieldName(): String {
val declaration = getField()
return runReadAction { declaration?.name } ?: "unknown"
}
override fun getEvaluationElement(): PsiElement? {
return getField()
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.DebuggerBundle
import com.intellij.ui.IdeBorderFactory
import com.intellij.util.ui.DialogUtil
import com.intellij.xdebugger.breakpoints.XLineBreakpoint
import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel
import com.intellij.xdebugger.impl.breakpoints.XBreakpointBase
import org.jetbrains.kotlin.idea.KotlinBundle
import java.awt.BorderLayout
import javax.swing.Box
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JPanel
import kotlin.properties.Delegates
class KotlinFieldBreakpointPropertiesPanel: XBreakpointCustomPropertiesPanel<XLineBreakpoint<KotlinPropertyBreakpointProperties>>() {
private var myWatchInitializationCheckBox: JCheckBox by Delegates.notNull()
private var myWatchAccessCheckBox: JCheckBox by Delegates.notNull()
private var myWatchModificationCheckBox: JCheckBox by Delegates.notNull()
override fun getComponent(): JComponent {
myWatchInitializationCheckBox = JCheckBox(KotlinBundle.message("debugger.field.watchpoints.properties.panel.field.initialization.label"))
myWatchAccessCheckBox = JCheckBox(KotlinBundle.message("debugger.field.watchpoints.properties.panel.field.access.label"))
myWatchModificationCheckBox = JCheckBox(KotlinBundle.message("debugger.field.watchpoints.properties.panel.field.modification.label"))
DialogUtil.registerMnemonic(myWatchInitializationCheckBox)
DialogUtil.registerMnemonic(myWatchAccessCheckBox)
DialogUtil.registerMnemonic(myWatchModificationCheckBox)
fun Box.addNewPanelForCheckBox(checkBox: JCheckBox) {
val panel = JPanel(BorderLayout())
panel.add(checkBox, BorderLayout.NORTH)
this.add(panel)
}
val watchBox = Box.createVerticalBox()
watchBox.addNewPanelForCheckBox(myWatchInitializationCheckBox)
watchBox.addNewPanelForCheckBox(myWatchAccessCheckBox)
watchBox.addNewPanelForCheckBox(myWatchModificationCheckBox)
val mainPanel = JPanel(BorderLayout())
val innerPanel = JPanel(BorderLayout())
innerPanel.add(watchBox, BorderLayout.CENTER)
innerPanel.add(Box.createHorizontalStrut(3), BorderLayout.WEST)
innerPanel.add(Box.createHorizontalStrut(3), BorderLayout.EAST)
mainPanel.add(innerPanel, BorderLayout.NORTH)
mainPanel.border = IdeBorderFactory.createTitledBorder(DebuggerBundle.message("label.group.watch.events"), true)
return mainPanel
}
override fun loadFrom(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>) {
myWatchInitializationCheckBox.isSelected = breakpoint.properties.WATCH_INITIALIZATION
myWatchAccessCheckBox.isSelected = breakpoint.properties.WATCH_ACCESS
myWatchModificationCheckBox.isSelected = breakpoint.properties.WATCH_MODIFICATION
}
override fun saveTo(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>) {
var changed = breakpoint.properties.WATCH_ACCESS != myWatchAccessCheckBox.isSelected
breakpoint.properties.WATCH_ACCESS = myWatchAccessCheckBox.isSelected
changed = breakpoint.properties.WATCH_MODIFICATION != myWatchModificationCheckBox.isSelected || changed
breakpoint.properties.WATCH_MODIFICATION = myWatchModificationCheckBox.isSelected
changed = breakpoint.properties.WATCH_INITIALIZATION != myWatchInitializationCheckBox.isSelected || changed
breakpoint.properties.WATCH_INITIALIZATION = myWatchInitializationCheckBox.isSelected
if (changed) {
(breakpoint as XBreakpointBase<*, *, *>).fireBreakpointChanged()
}
}
}
@@ -0,0 +1,178 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.ui.breakpoints.Breakpoint
import com.intellij.debugger.ui.breakpoints.BreakpointManager
import com.intellij.debugger.ui.breakpoints.BreakpointWithHighlighter
import com.intellij.debugger.ui.breakpoints.JavaBreakpointType
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.breakpoints.XBreakpoint
import com.intellij.xdebugger.breakpoints.XLineBreakpoint
import com.intellij.xdebugger.breakpoints.XLineBreakpointType
import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.debugger.breakpoints.dialog.AddFieldBreakpointDialog
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtDeclarationContainer
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtProperty
import javax.swing.JComponent
class KotlinFieldBreakpointType : JavaBreakpointType<KotlinPropertyBreakpointProperties>, XLineBreakpointType<KotlinPropertyBreakpointProperties>(
"kotlin-field", KotlinBundle.message("debugger.field.watchpoints.tab.title")
) {
override fun createJavaBreakpoint(project: Project, breakpoint: XBreakpoint<KotlinPropertyBreakpointProperties>): Breakpoint<KotlinPropertyBreakpointProperties> {
return KotlinFieldBreakpoint(project, breakpoint)
}
override fun canPutAt(file: VirtualFile, line: Int, project: Project): Boolean {
return canPutAt(file, line, project, this::class.java)
}
override fun getPriority() = 120
override fun createBreakpointProperties(file: VirtualFile, line: Int): KotlinPropertyBreakpointProperties? {
return KotlinPropertyBreakpointProperties()
}
override fun addBreakpoint(project: Project, parentComponent: JComponent?): XLineBreakpoint<KotlinPropertyBreakpointProperties>? {
var result: XLineBreakpoint<KotlinPropertyBreakpointProperties>? = null
val dialog = object : AddFieldBreakpointDialog(project) {
override fun validateData(): Boolean {
val className = className
if (className.isEmpty()) {
reportError(project, DebuggerBundle.message("error.field.breakpoint.class.name.not.specified"))
return false
}
val psiClass = JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project))
if (psiClass !is KtLightClass) {
reportError(project, "Couldn't find '$className' class")
return false
}
val fieldName = fieldName
if (fieldName.isEmpty()) {
reportError(project, DebuggerBundle.message("error.field.breakpoint.field.name.not.specified"))
return false
}
result = when (psiClass) {
is KtLightClassForFacade -> {
psiClass.files.asSequence().mapNotNull { createBreakpointIfPropertyExists(it, it, className, fieldName) }.firstOrNull()
}
is KtLightClassForSourceDeclaration -> {
val jetClass = psiClass.kotlinOrigin
createBreakpointIfPropertyExists(jetClass, jetClass.containingKtFile, className, fieldName)
}
else -> null
}
if (result == null) {
reportError(project, DebuggerBundle.message("error.field.breakpoint.field.not.found", className, fieldName, fieldName))
}
return result != null
}
}
dialog.show()
return result
}
private fun createBreakpointIfPropertyExists(
declaration: KtDeclarationContainer,
file: KtFile,
className: String,
fieldName: String
): XLineBreakpoint<KotlinPropertyBreakpointProperties>? {
val project = file.project
val property = declaration.declarations.firstOrNull { it is KtProperty && it.name == fieldName } ?: return null
val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return null
val line = document.getLineNumber(property.textOffset)
return runWriteAction {
XDebuggerManager.getInstance(project).breakpointManager.addLineBreakpoint(
this,
file.virtualFile.url,
line,
KotlinPropertyBreakpointProperties(fieldName, className)
)
}
}
private fun reportError(project: Project, message: String) {
Messages.showMessageDialog(project, message, DebuggerBundle.message("add.field.breakpoint.dialog.title"), Messages.getErrorIcon())
}
override fun isAddBreakpointButtonVisible() = true
override fun getMutedEnabledIcon() = AllIcons.Debugger.Db_muted_field_breakpoint
override fun getDisabledIcon() = AllIcons.Debugger.Db_disabled_field_breakpoint
override fun getEnabledIcon() = AllIcons.Debugger.Db_field_breakpoint
override fun getMutedDisabledIcon() = AllIcons.Debugger.Db_muted_disabled_field_breakpoint
override fun canBeHitInOtherPlaces() = true
override fun getShortText(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>): String? {
val properties = breakpoint.properties
val className = properties.myClassName
return if (!className.isEmpty()) className + "." + properties.myFieldName else properties.myFieldName
}
override fun createProperties(): KotlinPropertyBreakpointProperties? {
return KotlinPropertyBreakpointProperties()
}
override fun createCustomPropertiesPanel(): XBreakpointCustomPropertiesPanel<XLineBreakpoint<KotlinPropertyBreakpointProperties>>? {
return KotlinFieldBreakpointPropertiesPanel()
}
override fun getDisplayText(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>): String? {
val kotlinBreakpoint = BreakpointManager.getJavaBreakpoint(breakpoint) as? BreakpointWithHighlighter
return if (kotlinBreakpoint != null) {
kotlinBreakpoint.description
}
else {
super.getDisplayText(breakpoint)
}
}
override fun getEditorsProvider() = null
override fun createCustomRightPropertiesPanel(project: Project): XBreakpointCustomPropertiesPanel<XLineBreakpoint<KotlinPropertyBreakpointProperties>>? {
return KotlinBreakpointFiltersPanel(project)
}
override fun isSuspendThreadSupported() = true
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger.breakpoints
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.ui.breakpoints.LineBreakpoint
import com.intellij.openapi.project.Project
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.breakpoints.XBreakpoint
import com.intellij.xdebugger.breakpoints.XBreakpointProperties
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.ReferenceType
import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.idea.debugger.isDexDebug
class KotlinLineBreakpoint(
project: Project?,
xBreakpoint: XBreakpoint<out XBreakpointProperties<*>>?
) : LineBreakpoint<JavaLineBreakpointProperties>(project, xBreakpoint) {
override fun processClassPrepare(debugProcess: DebugProcess?, classType: ReferenceType?) {
val sourcePosition = xBreakpoint?.sourcePosition
if (classType != null && sourcePosition != null) {
if (!hasTargetLine(classType, sourcePosition)) {
return
}
}
super.processClassPrepare(debugProcess, classType)
}
/**
* Returns false if `classType` definitely does not contain a location for a given `sourcePosition`.
*/
private fun hasTargetLine(classType: ReferenceType, sourcePosition: XSourcePosition): Boolean {
val allLineLocations = DebuggerUtilsEx.allLineLocations(classType) ?: return true
if (classType.virtualMachine().isDexDebug()) {
return true
}
val fileName = sourcePosition.file.name
val lineNumber = sourcePosition.line + 1
for (location in allLineLocations) {
try {
val kotlinFileName = location.sourceName(KOTLIN_STRATA_NAME)
val kotlinLineNumber = location.lineNumber(KOTLIN_STRATA_NAME)
if (kotlinFileName == fileName && kotlinLineNumber == lineNumber) {
return true
}
} catch (e: AbsentInformationException) {
if (location.sourceName() == fileName && location.lineNumber() == lineNumber) {
return true
}
}
}
return false
}
}
@@ -0,0 +1,152 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints;
import com.intellij.debugger.SourcePosition;
import com.intellij.debugger.ui.breakpoints.Breakpoint;
import com.intellij.debugger.ui.breakpoints.BreakpointManager;
import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType;
import com.intellij.debugger.ui.breakpoints.LineBreakpoint;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.XLineBreakpoint;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.debugger.breakpoints.properties.JavaBreakpointProperties;
import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties;
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager;
import org.jetbrains.kotlin.psi.KtClassInitializer;
import org.jetbrains.kotlin.psi.KtFunction;
import java.util.List;
public class KotlinLineBreakpointType extends JavaLineBreakpointType {
public KotlinLineBreakpointType() {
super("kotlin-line", "Kotlin Line Breakpoints");
}
@NotNull
@Override
public Breakpoint<JavaLineBreakpointProperties> createJavaBreakpoint(
Project project, XBreakpoint<JavaLineBreakpointProperties> breakpoint
) {
return new KotlinLineBreakpoint(project, breakpoint);
}
@Override
public boolean matchesPosition(@NotNull LineBreakpoint<?> breakpoint, @NotNull SourcePosition position) {
JavaBreakpointProperties properties = getProperties(breakpoint);
if (properties == null || properties instanceof JavaLineBreakpointProperties) {
if (position instanceof KotlinPositionManager.KotlinReentrantSourcePosition) {
return false;
}
if (properties != null && ((JavaLineBreakpointProperties) properties).getLambdaOrdinal() == null) return true;
PsiElement containingMethod = getContainingMethod(breakpoint);
if (containingMethod == null) return false;
return inTheMethod(position, containingMethod);
}
return true;
}
@Override
@Nullable
public PsiElement getContainingMethod(@NotNull LineBreakpoint<?> breakpoint) {
SourcePosition position = breakpoint.getSourcePosition();
if (position == null) return null;
JavaBreakpointProperties properties = getProperties(breakpoint);
if (properties instanceof JavaLineBreakpointProperties) {
Integer ordinal = ((JavaLineBreakpointProperties) properties).getLambdaOrdinal();
PsiElement lambda = getLambdaByOrdinal(position, ordinal);
if (lambda != null) return lambda;
}
return getContainingMethod(position.getElementAt());
}
@Nullable
private static JavaBreakpointProperties getProperties(@NotNull LineBreakpoint<?> breakpoint) {
XBreakpoint<?> xBreakpoint = breakpoint.getXBreakpoint();
return xBreakpoint != null ? (JavaBreakpointProperties) xBreakpoint.getProperties() : null;
}
@Nullable
private static KtFunction getLambdaByOrdinal(SourcePosition position, Integer ordinal) {
if (ordinal != null && ordinal >= 0) {
List<KtFunction> lambdas = BreakpointTypeUtilsKt.getLambdasAtLineIfAny(position);
if (lambdas.size() > ordinal) {
return lambdas.get(ordinal);
}
}
return null;
}
@Nullable
public static PsiElement getContainingMethod(@Nullable PsiElement elem) {
//noinspection unchecked
return PsiTreeUtil.getParentOfType(elem, KtFunction.class, KtClassInitializer.class);
}
public static boolean inTheMethod(@NotNull SourcePosition pos, @NotNull PsiElement method) {
PsiElement elem = pos.getElementAt();
if (elem == null) return false;
return Comparing.equal(getContainingMethod(elem), method);
}
@Override
public boolean canPutAt(@NotNull VirtualFile file, int line, @NotNull Project project) {
return BreakpointTypeUtilsKt.canPutAt(file, line, project, getClass());
}
@NotNull
@Override
public List<JavaBreakpointVariant> computeVariants(@NotNull Project project, @NotNull XSourcePosition position) {
return BreakpointTypeUtilsKt.computeVariants(project, position, this);
}
@Nullable
@Override
public TextRange getHighlightRange(XLineBreakpoint<JavaLineBreakpointProperties> breakpoint) {
JavaLineBreakpointProperties properties = breakpoint.getProperties();
if (properties != null) {
Integer ordinal = properties.getLambdaOrdinal();
if (ordinal != null) {
Breakpoint javaBreakpoint = BreakpointManager.getJavaBreakpoint(breakpoint);
if (javaBreakpoint instanceof LineBreakpoint) {
SourcePosition position = ((LineBreakpoint) javaBreakpoint).getSourcePosition();
if (position != null) {
KtFunction lambda = getLambdaByOrdinal(position, ordinal);
if (lambda != null) {
return lambda.getTextRange();
}
}
}
}
}
return null;
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.util.xmlb.annotations.Attribute
import org.jetbrains.java.debugger.breakpoints.properties.JavaBreakpointProperties
class KotlinPropertyBreakpointProperties(
@Attribute var myFieldName: String = "",
@Attribute var myClassName: String = ""
): JavaBreakpointProperties<KotlinPropertyBreakpointProperties>() {
var WATCH_MODIFICATION: Boolean = true
var WATCH_ACCESS: Boolean = false
var WATCH_INITIALIZATION: Boolean = false
override fun getState() = this
override fun loadState(state: KotlinPropertyBreakpointProperties) {
super.loadState(state)
WATCH_MODIFICATION = state.WATCH_MODIFICATION
WATCH_ACCESS = state.WATCH_ACCESS
WATCH_INITIALIZATION = state.WATCH_INITIALIZATION
myFieldName = state.myFieldName
myClassName = state.myClassName
}
}
@@ -0,0 +1,150 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.impl.XSourcePositionImpl
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineNumber
import org.jetbrains.kotlin.idea.debugger.findElementAtLine
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import java.util.*
fun canPutAt(file: VirtualFile, line: Int, project: Project, breakpointTypeClass: Class<*>): Boolean {
val psiFile = PsiManager.getInstance(project).findFile(file)
if (psiFile == null || psiFile.virtualFile?.fileType != KotlinFileType.INSTANCE) {
return false
}
val document = FileDocumentManager.getInstance().getDocument(file) ?: return false
var result: Class<*>? = null
XDebuggerUtil.getInstance().iterateLine(project, document, line, fun (el: PsiElement): Boolean {
// avoid comments
if (el is PsiWhiteSpace || PsiTreeUtil.getParentOfType(el, PsiComment::class.java, false) != null) {
return true
}
var element = el
var parent = element.parent
while (parent != null) {
val offset = parent.textOffset
if (offset >= 0 && document.getLineNumber(offset) != line) break
element = parent
parent = element.parent
}
if (element is KtProperty || element is KtParameter) {
result = if ((element is KtParameter && element.hasValOrVar()) || (element is KtProperty && !element.isLocal)) {
KotlinFieldBreakpointType::class.java
}
else {
KotlinLineBreakpointType::class.java
}
return false
}
else {
result = KotlinLineBreakpointType::class.java
}
return true
})
return result == breakpointTypeClass
}
fun computeVariants(
project: Project,
position: XSourcePosition,
kotlinBreakpointType: KotlinLineBreakpointType
): List<JavaLineBreakpointType.JavaBreakpointVariant> {
val file = PsiManager.getInstance(project).findFile(position.file) as? KtFile ?: return emptyList()
val pos = SourcePosition.createFromLine(file, position.line)
val lambdas = getLambdasAtLineIfAny(pos)
if (lambdas.isEmpty()) return emptyList()
val result = LinkedList<JavaLineBreakpointType.JavaBreakpointVariant>()
val elementAt = pos.elementAt.parentsWithSelf.firstIsInstance<KtElement>()
val mainMethod = KotlinLineBreakpointType.getContainingMethod(elementAt)
if (mainMethod != null) {
result.add(
kotlinBreakpointType.LineJavaBreakpointVariant(
position,
CodeInsightUtils.getTopmostElementAtOffset(elementAt, pos.offset),
-1
)
)
}
lambdas.forEachIndexed { ordinal, lambda ->
val positionImpl = XSourcePositionImpl.createByElement(lambda.bodyExpression)
if (positionImpl != null) {
result.add(kotlinBreakpointType.LambdaJavaBreakpointVariant(positionImpl, lambda, ordinal))
}
}
result.add(kotlinBreakpointType.JavaBreakpointVariant(position))
return result
}
fun getLambdasAtLineIfAny(sourcePosition: SourcePosition): List<KtFunction> {
val file = sourcePosition.file as? KtFile ?: return emptyList()
val lineNumber = sourcePosition.line
return getLambdasAtLineIfAny(file, lineNumber)
}
fun getLambdasAtLineIfAny(file: KtFile, line: Int): List<KtFunction> {
val lineElement = findElementAtLine(file, line) as? KtElement ?: return emptyList()
val start = lineElement.startOffset
val end = lineElement.endOffset
val allLiterals = CodeInsightUtils.
findElementsOfClassInRange(file, start, end, KtFunction::class.java)
.filterIsInstance<KtFunction>()
// filter function literals and functional expressions
.filter { it is KtFunctionLiteral || it.name == null }
.toSet()
return allLiterals.filter {
val statement = it.bodyBlockExpression?.statements?.firstOrNull() ?: it
statement.getLineNumber() == line && statement.getLineNumber(false) == line
}
}
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.breakpoints.dialog.AddFieldBreakpointDialog">
<grid id="dbe86" binding="myPanel" layout-manager="GridLayoutManager" row-count="6" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="6">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="74" y="134" width="245" height="152"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="9636d" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myClassChooser">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="c2ef3" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myFieldChooser">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<xy id="3cfba" layout-manager="XYLayout" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="5" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="2" fill="1" indent="0" use-parent-layout="false">
<minimum-size width="-1" height="1"/>
<maximum-size width="-1" height="1"/>
</grid>
</constraints>
<properties/>
<border type="bevel-raised"/>
<children/>
</xy>
<vspacer id="339be">
<constraints>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="e8c64" class="javax.swing.JLabel">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="messages/DebuggerBundle" key="label.add.field.breakpoint.dialog.field.name"/>
</properties>
</component>
<component id="c159b" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="messages/DebuggerBundle" key="label.add.field.breakpoint.dialog.fq.name"/>
</properties>
</component>
</children>
</grid>
</form>
@@ -0,0 +1,146 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints.dialog;
import com.intellij.debugger.DebuggerBundle;
import com.intellij.ide.util.MemberChooser;
import com.intellij.ide.util.TreeClassChooser;
import com.intellij.ide.util.TreeClassChooserFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.idea.core.util.DescriptorMemberChooserObject;
import org.jetbrains.kotlin.idea.core.util.UiUtilsKt;
import org.jetbrains.kotlin.psi.KtProperty;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
public abstract class AddFieldBreakpointDialog extends DialogWrapper {
private final Project myProject;
private JPanel myPanel;
private TextFieldWithBrowseButton myFieldChooser;
private TextFieldWithBrowseButton myClassChooser;
public AddFieldBreakpointDialog(Project project) {
super(project, true);
myProject = project;
setTitle(DebuggerBundle.message("add.field.breakpoint.dialog.title"));
init();
}
@Override
protected JComponent createCenterPanel() {
UiUtilsKt.onTextChange(
myClassChooser.getTextField(),
(DocumentEvent e) -> {
updateUI();
return Unit.INSTANCE;
}
);
myClassChooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
PsiClass currentClass = getSelectedClass();
TreeClassChooser chooser = TreeClassChooserFactory.getInstance(myProject).createAllProjectScopeChooser(
DebuggerBundle.message("add.field.breakpoint.dialog.classchooser.title"));
if (currentClass != null) {
PsiFile containingFile = currentClass.getContainingFile();
if (containingFile != null) {
PsiDirectory containingDirectory = containingFile.getContainingDirectory();
if (containingDirectory != null) {
chooser.selectDirectory(containingDirectory);
}
}
}
chooser.showDialog();
PsiClass selectedClass = chooser.getSelected();
if (selectedClass != null) {
myClassChooser.setText(selectedClass.getQualifiedName());
}
}
});
myFieldChooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
PsiClass selectedClass = getSelectedClass();
DescriptorMemberChooserObject[] properties = FieldBreakpointDialogUtilKt.collectProperties(selectedClass);
MemberChooser<DescriptorMemberChooserObject> chooser = new MemberChooser<DescriptorMemberChooserObject>(properties, false, false, myProject);
chooser.setTitle(DebuggerBundle.message("add.field.breakpoint.dialog.field.chooser.title", properties.length));
chooser.setCopyJavadocVisible(false);
chooser.show();
List<DescriptorMemberChooserObject> selectedElements = chooser.getSelectedElements();
if (selectedElements != null && selectedElements.size() == 1) {
KtProperty field = (KtProperty) selectedElements.get(0).getElement();
myFieldChooser.setText(field.getName());
}
}
});
myFieldChooser.setEnabled(false);
return myPanel;
}
private void updateUI() {
PsiClass selectedClass = getSelectedClass();
myFieldChooser.setEnabled(selectedClass != null);
}
private PsiClass getSelectedClass() {
PsiManager psiManager = PsiManager.getInstance(myProject);
String classQName = myClassChooser.getText();
if (classQName == null || classQName.isEmpty()) {
return null;
}
return JavaPsiFacade.getInstance(psiManager.getProject()).findClass(classQName, GlobalSearchScope.allScope(myProject));
}
@Override
public JComponent getPreferredFocusedComponent() {
return myClassChooser.getTextField();
}
public String getClassName() {
return myClassChooser.getText();
}
@Override
protected String getDimensionServiceKey() {
return "#com.intellij.debugger.ui.breakpoints.BreakpointsConfigurationDialogFactory.BreakpointsConfigurationDialog.AddFieldBreakpointDialog";
}
public String getFieldName() {
return myFieldChooser.getText();
}
protected abstract boolean validateData();
@Override
protected void doOKAction() {
if (validateData()) {
super.doOKAction();
}
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints.dialog
import com.intellij.psi.PsiClass
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor
import org.jetbrains.kotlin.idea.core.util.DescriptorMemberChooserObject
import org.jetbrains.kotlin.psi.KtProperty
fun PsiClass.collectProperties(): Array<DescriptorMemberChooserObject> {
if (this is KtLightClassForFacade) {
val result = arrayListOf<DescriptorMemberChooserObject>()
this.files.forEach {
it.declarations.filterIsInstance<KtProperty>().forEach {
result.add(DescriptorMemberChooserObject(it, it.unsafeResolveToDescriptor()))
}
}
return result.toTypedArray()
}
if (this is KtLightClass) {
val origin = this.kotlinOrigin
if (origin != null) {
return origin.declarations.filterIsInstance<KtProperty>().map {
DescriptorMemberChooserObject(it, it.unsafeResolveToDescriptor())
}.toTypedArray()
}
}
return emptyArray()
}
@@ -0,0 +1,256 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.psi.PsiElement
import com.sun.jdi.*
import com.sun.tools.jdi.LocalVariableImpl
import org.jetbrains.kotlin.codegen.binding.CodegenBinding
import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousClass
import org.jetbrains.kotlin.codegen.coroutines.DO_RESUME_METHOD_NAME
import org.jetbrains.kotlin.codegen.coroutines.INVOKE_SUSPEND_METHOD_NAME
import org.jetbrains.kotlin.codegen.coroutines.continuationAsmTypes
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.core.util.getLineEndOffset
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.org.objectweb.asm.Type as AsmType
import java.util.*
fun Location.isInKotlinSources(): Boolean {
val declaringType = declaringType()
val fileExtension = declaringType.safeSourceName()?.substringAfterLast('.')?.toLowerCase() ?: ""
return fileExtension in KotlinFileTypeFactory.KOTLIN_EXTENSIONS || declaringType.containsKotlinStrata()
}
fun ReferenceType.containsKotlinStrata() = availableStrata().contains(KOTLIN_STRATA_NAME)
fun isInsideInlineArgument(
inlineArgument: KtFunction,
location: Location,
debugProcess: DebugProcessImpl,
bindingContext: BindingContext = KotlinDebuggerCaches.getOrCreateTypeMapper(inlineArgument).bindingContext
): Boolean {
val visibleVariables = location.visibleVariables(debugProcess)
val markerLocalVariables = visibleVariables.filter { it.name().startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT) }
val context = KotlinDebuggerCaches.getOrCreateTypeMapper(inlineArgument).bindingContext
val lambdaOrdinal = runReadAction { lambdaOrdinalByArgument(inlineArgument, context) }
val functionName = runReadAction { functionNameByArgument(inlineArgument, context) }
return markerLocalVariables
.map { it.name().drop(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT.length) }
.any { variableName ->
if (variableName.startsWith("-")) {
val lambdaClassName = asmTypeForAnonymousClass(bindingContext, inlineArgument)
.internalName.substringAfterLast("/")
variableName == "-$functionName-$lambdaClassName"
} else {
// For Kotlin up to 1.3.10
lambdaOrdinalByLocalVariable(variableName) == lambdaOrdinal
&& functionNameByLocalVariable(variableName) == functionName
}
}
}
fun <T : Any> DebugProcessImpl.invokeInManagerThread(f: (DebuggerContextImpl) -> T?): T? {
var result: T? = null
val command: DebuggerCommandImpl = object : DebuggerCommandImpl() {
override fun action() {
result = runReadAction { f(debuggerContext) }
}
}
when {
DebuggerManagerThreadImpl.isManagerThread() ->
managerThread.invoke(command)
else ->
managerThread.invokeAndWait(command)
}
return result
}
private fun lambdaOrdinalByArgument(elementAt: KtFunction, context: BindingContext): Int {
val type = CodegenBinding.asmTypeForAnonymousClass(context, elementAt)
return type.className.substringAfterLast("$").toInt()
}
private fun functionNameByArgument(elementAt: KtFunction, context: BindingContext): String {
val inlineArgumentDescriptor = InlineUtil.getInlineArgumentDescriptor(elementAt, context)
return inlineArgumentDescriptor?.containingDeclaration?.name?.asString() ?: "unknown"
}
private fun Location.visibleVariables(debugProcess: DebugProcessImpl): List<LocalVariable> {
val stackFrame = MockStackFrame(this, debugProcess.virtualMachineProxy.virtualMachine)
return stackFrame.visibleVariables()
}
private fun lambdaOrdinalByLocalVariable(name: String): Int {
try {
val nameWithoutPrefix = name.removePrefix(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT)
return Integer.parseInt(nameWithoutPrefix.substringBefore("$", nameWithoutPrefix))
}
catch(e: NumberFormatException) {
return 0
}
}
private fun functionNameByLocalVariable(name: String): String {
val nameWithoutPrefix = name.removePrefix(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT)
return nameWithoutPrefix.substringAfterLast("$", "unknown")
}
private class MockStackFrame(private val location: Location, private val vm: VirtualMachine) : StackFrame {
private var visibleVariables: Map<String, LocalVariable>? = null
override fun location() = location
override fun thread() = null
override fun thisObject() = null
private fun createVisibleVariables() {
if (visibleVariables == null) {
val allVariables = location.method().safeVariables() ?: emptyList()
val map = HashMap<String, LocalVariable>(allVariables.size)
for (allVariable in allVariables) {
val variable = allVariable as LocalVariableImpl
val name = variable.name()
if (variable.isVisible(this)) {
map.put(name, variable)
}
}
visibleVariables = map
}
}
override fun visibleVariables(): List<LocalVariable> {
createVisibleVariables()
val mapAsList = ArrayList(visibleVariables!!.values)
Collections.sort(mapAsList)
return mapAsList
}
override fun visibleVariableByName(name: String): LocalVariable? {
createVisibleVariables()
return visibleVariables!![name]
}
override fun getValue(variable: LocalVariable) = null
override fun getValues(variables: List<LocalVariable>): Map<LocalVariable, Value> = emptyMap()
override fun setValue(variable: LocalVariable, value: Value) {
}
override fun getArgumentValues(): List<Value> = emptyList()
override fun virtualMachine() = vm
}
private const val DO_RESUME_SIGNATURE = "(Ljava/lang/Object;Ljava/lang/Throwable;)Ljava/lang/Object;"
private const val INVOKE_SUSPEND_SIGNATURE = "(Ljava/lang/Object;)Ljava/lang/Object;"
fun isInSuspendMethod(location: Location): Boolean {
val method = location.method()
val signature = method.signature()
for (continuationAsmType in continuationAsmTypes()) {
if (signature.contains(continuationAsmType.toString()) ||
(method.name() == DO_RESUME_METHOD_NAME && signature == DO_RESUME_SIGNATURE) ||
(method.name() == INVOKE_SUSPEND_METHOD_NAME && signature == INVOKE_SUSPEND_SIGNATURE)
) return true
}
return false
}
fun suspendFunctionFirstLineLocation(location: Location): Int? {
if (!isInSuspendMethod(location)) {
return null
}
val lineNumber = location.method().location()?.lineNumber()
if (lineNumber == -1) {
return null
}
return lineNumber
}
fun isOnSuspendReturnOrReenter(location: Location): Boolean {
val suspendStartLineNumber = suspendFunctionFirstLineLocation(location) ?: return false
return suspendStartLineNumber == location.lineNumber()
}
fun isLastLineLocationInMethod(location: Location): Boolean {
val knownLines = location.method().safeAllLineLocations().map { it.lineNumber() }.filter { it != -1 }
if (knownLines.isEmpty()) {
return false
}
return knownLines.max() == location.lineNumber()
}
fun isOneLineMethod(location: Location): Boolean {
val allLineLocations = location.method().safeAllLineLocations()
val firstLine = allLineLocations.firstOrNull()?.lineNumber()
val lastLine = allLineLocations.lastOrNull()?.lineNumber()
return firstLine != null && firstLine == lastLine
}
fun findElementAtLine(file: KtFile, line: Int): PsiElement? {
val lineStartOffset = file.getLineStartOffset(line) ?: return null
val lineEndOffset = file.getLineEndOffset(line) ?: return null
var topMostElement: PsiElement? = null
var elementAt: PsiElement?
for (offset in lineStartOffset until lineEndOffset) {
elementAt = file.findElementAt(offset)
if (elementAt != null) {
topMostElement = CodeInsightUtils.getTopmostElementAtOffset(elementAt, offset)
if (topMostElement is KtElement) {
break
}
}
}
return topMostElement
}
fun findCallByEndToken(element: PsiElement): KtCallExpression? {
if (element is KtElement) return null
return when (element.node.elementType) {
KtTokens.RPAR -> (element.parent as? KtValueArgumentList)?.parent as? KtCallExpression
KtTokens.RBRACE -> {
val braceParent = CodeInsightUtils.getTopParentWithEndOffset(element, KtCallExpression::class.java)
when (braceParent) {
is KtCallExpression -> braceParent
is KtLambdaArgument -> braceParent.parent as? KtCallExpression
is KtValueArgument -> (braceParent.parent as? KtValueArgumentList)?.parent as? KtCallExpression
else -> null
}
}
else -> null
}
}
val DebuggerContextImpl.canRunEvaluation: Boolean
get() = debugProcess?.canRunEvaluation ?: false
val DebugProcessImpl.canRunEvaluation: Boolean
get() = suspendManager.pausedContext != null
@@ -0,0 +1,102 @@
/*
* Copyright 2000-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.debugger.evaluate
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.ContextUtil
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.evaluation.expression.ExpressionEvaluator
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.ui.EditorEvaluationCommand
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.CommonClassNames
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.ClassType
import com.sun.jdi.Value
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.debugger.getClassDescriptor
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.org.objectweb.asm.Type as AsmType
abstract class KotlinRuntimeTypeEvaluator(
editor: Editor?,
expression: KtExpression,
context: DebuggerContextImpl,
indicator: ProgressIndicator
) : EditorEvaluationCommand<KotlinType>(editor, expression, context, indicator) {
override fun threadAction() {
var type: KotlinType? = null
try {
type = evaluate()
} catch (ignored: ProcessCanceledException) {
} catch (ignored: EvaluateException) {
} finally {
typeCalculationFinished(type)
}
}
protected abstract fun typeCalculationFinished(type: KotlinType?)
override fun evaluate(evaluationContext: EvaluationContextImpl): KotlinType? {
val project = evaluationContext.project
val evaluator = DebuggerInvocationUtil.commitAndRunReadAction<ExpressionEvaluator>(project) {
val codeFragment = KtPsiFactory(myElement.project)
.createExpressionCodeFragment(myElement.text, myElement.containingFile.context)
val codeFragmentFactory = DebuggerUtilsEx.getCodeFragmentFactory(codeFragment.context, KotlinFileType.INSTANCE)
codeFragmentFactory.evaluatorBuilder.build(codeFragment, ContextUtil.getSourcePosition(evaluationContext))
}
val value = evaluator.evaluate(evaluationContext)
if (value != null) {
return runReadAction { getCastableRuntimeType(evaluationContext.debugProcess.searchScope, value) }
}
throw EvaluateExceptionUtil.createEvaluateException(DebuggerBundle.message("evaluation.error.surrounded.expression.null"))
}
companion object {
private fun getCastableRuntimeType(scope: GlobalSearchScope, value: Value): KotlinType? {
val myValue = value.asValue()
var psiClass = myValue.asmType.getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
val type = value.type()
if (type is ClassType) {
val superclass = type.superclass()
if (superclass != null && CommonClassNames.JAVA_LANG_OBJECT != superclass.name()) {
psiClass = AsmType.getType(superclass.signature()).getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
}
for (interfaceType in type.interfaces()) {
psiClass = AsmType.getType(interfaceType.signature()).getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
}
}
return null
}
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.filter
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.ui.classFilter.ClassFilter
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings
private val KOTLIN_STDLIB_FILTER = "kotlin.*"
fun addKotlinStdlibDebugFilterIfNeeded() {
if (!KotlinDebuggerSettings.getInstance().DEBUG_IS_FILTER_FOR_STDLIB_ALREADY_ADDED) {
val settings = DebuggerSettings.getInstance()!!
val newFilters = (settings.steppingFilters + ClassFilter(KOTLIN_STDLIB_FILTER))
settings.steppingFilters = newFilters
KotlinDebuggerSettings.getInstance().DEBUG_IS_FILTER_FOR_STDLIB_ALREADY_ADDED = true
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.filter
import com.intellij.ui.classFilter.ClassFilter
import com.intellij.ui.classFilter.DebuggerClassFilterProvider
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings
private val FILTERS = listOf(
ClassFilter("kotlin.jvm*"),
ClassFilter("kotlin.reflect*"),
ClassFilter("kotlin.NoWhenBranchMatchedException"),
ClassFilter("kotlin.TypeCastException"),
ClassFilter("kotlin.KotlinNullPointerException")
)
class KotlinDebuggerInternalClassesFilterProvider : DebuggerClassFilterProvider {
override fun getFilters(): List<ClassFilter>? {
return if (KotlinDebuggerSettings.getInstance().DEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES) FILTERS else listOf()
}
}
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.filter
import com.intellij.debugger.engine.SyntheticTypeComponentProvider
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.FqNameUnsafe
import org.jetbrains.org.objectweb.asm.Opcodes
import kotlin.jvm.internal.FunctionReference
import kotlin.jvm.internal.PropertyReference
class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvider {
override fun isSynthetic(typeComponent: TypeComponent?): Boolean {
if (typeComponent !is Method) return false
val containingType = typeComponent.declaringType()
val typeName = containingType.name()
if (!FqNameUnsafe.isValid(typeName)) return false
// TODO: this is most likely not necessary since KT-28453 is fixed, but still can be useful when debugging old compiled code
if (containingType.isCallableReferenceSyntheticClass()) {
return true
}
try {
if (typeComponent.isDelegateToDefaultInterfaceImpl()) return true
if (typeComponent.location()?.lineNumber() != 1) return false
if (typeComponent.allLineLocations().any { it.lineNumber() != 1 }) {
return false
}
return !typeComponent.declaringType().allLineLocations().any { it.lineNumber() != 1 }
}
catch(e: AbsentInformationException) {
return false
}
catch(e: UnsupportedOperationException) {
return false
}
}
private tailrec fun ReferenceType?.isCallableReferenceSyntheticClass(): Boolean {
if (this !is ClassType) return false
val superClass = this.superclass() ?: return false
val superClassName = superClass.name()
if (superClassName == PropertyReference::class.java.name || superClassName == FunctionReference::class.java.name) {
return true
}
// The direct supertype may be PropertyReference0 or something
return if (superClassName.startsWith("kotlin.jvm.internal."))
superClass.isCallableReferenceSyntheticClass()
else
false
}
private fun Method.isDelegateToDefaultInterfaceImpl(): Boolean {
if (safeAllLineLocations().size != 1) return false
if (!virtualMachine().canGetBytecodes()) return false
if (!hasOnlyInvokeStatic(this)) return false
return hasInterfaceWithImplementation(this)
}
private val LOAD_INSTRUCTIONS_WITH_INDEX = Opcodes.ILOAD.toByte()..Opcodes.ALOAD.toByte()
private val LOAD_INSTRUCTIONS = (Opcodes.ALOAD + 1).toByte()..(Opcodes.IALOAD - 1).toByte()
private val RETURN_INSTRUCTIONS = Opcodes.IRETURN.toByte()..Opcodes.RETURN.toByte()
// Check that method contains only load and invokeStatic instructions. Note that if after load goes ldc instruction it could be checkParametersNotNull method invocation
private fun hasOnlyInvokeStatic(m: Method): Boolean {
val bytecodes = m.bytecodes()
var i = 0
var isALoad0BeforeStaticCall = false
while (i < bytecodes.size) {
val instr = bytecodes[i]
when {
instr == 42.toByte() /* ALOAD_0 */ -> {
i += 1
isALoad0BeforeStaticCall = true
}
instr in LOAD_INSTRUCTIONS_WITH_INDEX || instr in LOAD_INSTRUCTIONS -> {
i += 1
if (instr in LOAD_INSTRUCTIONS_WITH_INDEX) i += 1
val nextInstr = bytecodes[i]
if (nextInstr == Opcodes.LDC.toByte()) {
i += 2
isALoad0BeforeStaticCall = false
}
}
instr == Opcodes.INVOKESTATIC.toByte() -> {
i += 3
if (isALoad0BeforeStaticCall && i == (bytecodes.size - 1)) {
val nextInstr = bytecodes[i]
return nextInstr in RETURN_INSTRUCTIONS
}
}
else -> return false
}
}
return false
}
// TODO: class DefaultImpl can be not loaded
private fun hasInterfaceWithImplementation(method: Method): Boolean {
val declaringType = method.declaringType() as? ClassType ?: return false
val interfaces = declaringType.allInterfaces()
val vm = declaringType.virtualMachine()
val traitImpls = interfaces.flatMap { vm.classesByName(it.name() + JvmAbi.DEFAULT_IMPLS_SUFFIX) }
return traitImpls.any { !it.methodsByName(method.name()).isEmpty() }
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.render
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.ui.impl.watch.FieldDescriptorImpl
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiExpression
import com.sun.jdi.*
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.Name
class DelegatedPropertyFieldDescriptor(
project: Project,
objectRef: ObjectReference,
val delegate: Field,
private val renderDelegatedProperty: Boolean
) : FieldDescriptorImpl(project, objectRef, delegate) {
override fun calcValue(evaluationContext: EvaluationContextImpl?): Value? {
if (evaluationContext == null) return null
if (!renderDelegatedProperty) return super.calcValue(evaluationContext)
val method = findGetterForDelegatedProperty()
val threadReference = evaluationContext.suspendContext.thread?.threadReference
if (method == null || threadReference == null) {
return super.calcValue(evaluationContext)
}
try {
return evaluationContext.debugProcess.invokeInstanceMethod(
evaluationContext,
`object`,
method,
listOf<Nothing>(),
evaluationContext.suspendContext.suspendPolicy
)
}
catch(e: EvaluateException) {
return e.exceptionFromTargetVM
}
}
override fun getName(): String {
return delegate.name().removeSuffix(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX)
}
override fun getDescriptorEvaluation(context: DebuggerContext?): PsiExpression? {
return null
}
private fun findGetterForDelegatedProperty(): Method? {
val fieldName = name
if (!Name.isValidIdentifier(fieldName)) return null
return `object`.referenceType().methodsByName(JvmAbi.getterName(fieldName))?.firstOrNull()
}
override fun getDeclaredType(): String? {
val getter = findGetterForDelegatedProperty() ?: return null
val returnType = try {
getter.returnType()
} catch (e: ClassNotLoadedException) {
// Behavior copied from LocalVariableDescriptorImpl (in platform)
return "<unknown>"
}
return returnType?.name()
}
}
@@ -0,0 +1,141 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.render
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.settings.NodeRendererSettings
import com.intellij.debugger.ui.impl.watch.MessageDescriptor
import com.intellij.debugger.ui.impl.watch.NodeManagerImpl
import com.intellij.debugger.ui.tree.DebuggerTreeNode
import com.intellij.debugger.ui.tree.ValueDescriptor
import com.intellij.debugger.ui.tree.render.ChildrenBuilder
import com.intellij.debugger.ui.tree.render.ClassRenderer
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
import com.intellij.openapi.diagnostic.Logger
import com.intellij.xdebugger.settings.XDebuggerSettingsManager
import com.sun.jdi.*
import com.sun.jdi.Type
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings
import org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState
import org.jetbrains.kotlin.idea.debugger.canRunEvaluation
import org.jetbrains.kotlin.load.java.JvmAbi
import java.util.*
import com.sun.jdi.Type as JdiType
import org.jetbrains.org.objectweb.asm.Type as AsmType
private val LOG = Logger.getInstance(KotlinClassWithDelegatedPropertyRenderer::class.java)
private fun notPreparedClassMessage(referenceType: ReferenceType) =
"$referenceType ${referenceType.isPrepared} ${referenceType.sourceName()}"
class KotlinClassWithDelegatedPropertyRenderer : ClassRenderer() {
private val rendererSettings = NodeRendererSettings.getInstance()
override fun isApplicable(jdiType: Type?): Boolean {
if (!super.isApplicable(jdiType)) return false
if (jdiType !is ReferenceType) return false
if (!jdiType.isPrepared) {
LOG.info(notPreparedClassMessage(jdiType))
return false
}
try {
return jdiType.allFields().any { it.name().endsWith(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX) }
} catch (notPrepared: ClassNotPreparedException) {
LOG.error(notPreparedClassMessage(jdiType), notPrepared)
}
return false
}
override fun calcLabel(
descriptor: ValueDescriptor,
evaluationContext: EvaluationContext,
listener: DescriptorLabelListener
): String {
val res = calcToStringLabel(descriptor, evaluationContext, listener)
if (res != null) {
return res
}
return super.calcLabel(descriptor, evaluationContext, listener)
}
private fun calcToStringLabel(
descriptor: ValueDescriptor, evaluationContext: EvaluationContext,
listener: DescriptorLabelListener
): String? {
val toStringRenderer = rendererSettings.toStringRenderer
if (toStringRenderer.isEnabled && DebuggerManagerEx.getInstanceEx(evaluationContext.project).context.canRunEvaluation) {
if (toStringRenderer.isApplicable(descriptor.type)) {
return toStringRenderer.calcLabel(descriptor, evaluationContext, listener)
}
}
return null
}
override fun buildChildren(value: Value?, builder: ChildrenBuilder, context: EvaluationContext) {
DebuggerManagerThreadImpl.assertIsManagerThread()
if (value !is ObjectReference) return
val nodeManager = builder.nodeManager!!
val nodeDescriptorFactory = builder.descriptorManager!!
val fields = value.referenceType().allFields()
if (fields.isEmpty()) {
builder.setChildren(listOf(nodeManager.createMessageNode(MessageDescriptor.CLASS_HAS_NO_FIELDS.label)))
return
}
val children = ArrayList<DebuggerTreeNode>()
for (field in fields) {
if (!shouldDisplay(context, value, field)) {
continue
}
val fieldDescriptor = nodeDescriptorFactory.getFieldDescriptor(builder.parentDescriptor, value, field)
if (field.name().endsWith(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX)) {
val shouldRenderDelegatedProperty = KotlinDebuggerSettings.getInstance().DEBUG_RENDER_DELEGATED_PROPERTIES
if (shouldRenderDelegatedProperty && !ToggleKotlinVariablesState.getService().kotlinVariableView) {
children.add(nodeManager.createNode(fieldDescriptor, context))
}
val delegatedPropertyDescriptor = DelegatedPropertyFieldDescriptor(
context.debugProcess.project!!,
value,
field,
shouldRenderDelegatedProperty
)
children.add(nodeManager.createNode(delegatedPropertyDescriptor, context))
} else {
children.add(nodeManager.createNode(fieldDescriptor, context))
}
}
if (XDebuggerSettingsManager.getInstance()!!.dataViewSettings.isSortValues) {
children.sortedWith(NodeManagerImpl.getNodeComparator())
}
builder.setChildren(children)
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.RequestHint;
import com.intellij.debugger.engine.SuspendContextImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.sun.jdi.request.StepRequest;
import org.jetbrains.annotations.NotNull;
public class DebugProcessImplHelper {
public static DebugProcessImpl.StepOverCommand createStepOverCommandWithCustomFilter(
SuspendContextImpl suspendContext,
boolean ignoreBreakpoints,
KotlinSuspendCallStepOverFilter methodFilter
) {
DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
return debugProcess.new StepOverCommand(suspendContext, ignoreBreakpoints, StepRequest.STEP_LINE) {
@NotNull
@Override
protected RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread) {
@SuppressWarnings("MagicConstant")
RequestHint hint = new RequestHintWithMethodFilter(stepThread, suspendContext, StepRequest.STEP_OVER, methodFilter);
hint.setRestoreBreakpoints(ignoreBreakpoints);
hint.setIgnoreFilters(ignoreBreakpoints || debugProcess.getSession().shouldIgnoreSteppingFilters());
return hint;
}
};
}
}
@@ -0,0 +1,211 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.SuspendContextImpl;
import com.intellij.debugger.engine.evaluation.EvaluateException;
import com.intellij.debugger.impl.DebuggerUtilsEx;
import com.intellij.debugger.jdi.StackFrameProxyImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.ui.classFilter.DebuggerClassFilterProvider;
import com.sun.jdi.Location;
import com.sun.jdi.ObjectCollectedException;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.StepRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.debugger.NoStrataPositionManagerHelperKt;
import org.jetbrains.kotlin.psi.KtFunctionLiteral;
import org.jetbrains.kotlin.psi.KtNamedFunction;
import java.util.ArrayList;
import java.util.List;
public class DebuggerSteppingHelper {
private static Logger LOG = Logger.getInstance(DebuggerSteppingHelper.class);
public static DebugProcessImpl.ResumeCommand createStepOverCommand(
final SuspendContextImpl suspendContext,
final boolean ignoreBreakpoints,
final KotlinSteppingCommandProvider.KotlinSourcePosition kotlinSourcePosition
) {
final DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
return debugProcess.new ResumeCommand(suspendContext) {
@Override
public void contextAction() {
boolean isDexDebug = NoStrataPositionManagerHelperKt.isDexDebug(suspendContext.getDebugProcess());
try {
StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
if (frameProxy != null) {
Action action = KotlinSteppingCommandProviderKt.getStepOverAction(
frameProxy.location(),
kotlinSourcePosition,
frameProxy,
isDexDebug
);
createStepRequest(
suspendContext, getContextThread(),
debugProcess.getVirtualMachineProxy().eventRequestManager(),
StepRequest.STEP_LINE, StepRequest.STEP_OUT);
action.apply(debugProcess, suspendContext, ignoreBreakpoints);
return;
}
debugProcess.createStepOutCommand(suspendContext).contextAction();
}
catch (EvaluateException e) {
LOG.error(e);
}
}
};
}
public static DebugProcessImpl.ResumeCommand createStepOutCommand(
final SuspendContextImpl suspendContext,
final boolean ignoreBreakpoints,
final List<KtNamedFunction> inlineFunctions,
final KtFunctionLiteral inlineArgument
) {
final DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
return debugProcess.new ResumeCommand(suspendContext) {
@Override
public void contextAction() {
try {
StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
if (frameProxy != null) {
Action action = KotlinSteppingCommandProviderKt.getStepOutAction(
frameProxy.location(),
suspendContext,
inlineFunctions,
inlineArgument
);
createStepRequest(
suspendContext, getContextThread(),
debugProcess.getVirtualMachineProxy().eventRequestManager(),
StepRequest.STEP_LINE, StepRequest.STEP_OUT);
action.apply(debugProcess, suspendContext, ignoreBreakpoints);
return;
}
debugProcess.createStepOverCommand(suspendContext, ignoreBreakpoints).contextAction();
}
catch (EvaluateException ignored) {
}
}
};
}
// copied from DebugProcessImpl.doStep
private static void createStepRequest(
@NotNull SuspendContextImpl suspendContext,
@Nullable ThreadReferenceProxyImpl stepThread,
@NotNull EventRequestManager requestManager,
int size, int depth
) {
if (stepThread == null) {
return;
}
try {
ThreadReference stepThreadReference = stepThread.getThreadReference();
requestManager.deleteEventRequests(requestManager.stepRequests());
StepRequest stepRequest = requestManager.createStepRequest(stepThreadReference, size, depth);
List<ClassFilter> activeFilters = getActiveFilters();
if (!activeFilters.isEmpty()) {
String currentClassName = getCurrentClassName(stepThread);
if (currentClassName == null || !DebuggerUtilsEx.isFiltered(currentClassName, activeFilters)) {
// add class filters
for (ClassFilter filter : activeFilters) {
stepRequest.addClassExclusionFilter(filter.getPattern());
}
}
}
// suspend policy to match the suspend policy of the context:
// if all threads were suspended, then during stepping all the threads must be suspended
// if only event thread were suspended, then only this particular thread must be suspended during stepping
stepRequest.setSuspendPolicy(suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD
? EventRequest.SUSPEND_EVENT_THREAD
: EventRequest.SUSPEND_ALL);
stepRequest.enable();
}
catch (ObjectCollectedException ignored) {
}
}
// copied from DebugProcessImpl.getActiveFilters
@NotNull
private static List<ClassFilter> getActiveFilters() {
List<ClassFilter> activeFilters = new ArrayList<ClassFilter>();
DebuggerSettings settings = DebuggerSettings.getInstance();
if (settings.TRACING_FILTERS_ENABLED) {
for (ClassFilter filter : settings.getSteppingFilters()) {
if (filter.isEnabled()) {
activeFilters.add(filter);
}
}
}
for (DebuggerClassFilterProvider provider : Extensions.getExtensions(DebuggerClassFilterProvider.EP_NAME)) {
for (ClassFilter filter : provider.getFilters()) {
if (filter.isEnabled()) {
activeFilters.add(filter);
}
}
}
return activeFilters;
}
// copied from DebugProcessImpl.getActiveFilters
@Nullable
private static String getCurrentClassName(ThreadReferenceProxyImpl thread) {
try {
if (thread != null && thread.frameCount() > 0) {
StackFrameProxyImpl stackFrame = thread.frame(0);
if (stackFrame != null) {
Location location = stackFrame.location();
ReferenceType referenceType = location == null ? null : location.declaringType();
if (referenceType != null) {
return referenceType.name();
}
}
}
}
catch (EvaluateException ignored) {
}
return null;
}
}
@@ -0,0 +1,95 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.NamedMethodFilter
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.util.Range
import com.sun.jdi.Location
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.DECLARATION
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypesAndPredicate
import org.jetbrains.kotlin.resolve.DescriptorUtils
class KotlinBasicStepMethodFilter(
private val declarationPtr: SmartPsiElementPointer<KtDeclaration>?,
private val isInvoke: Boolean,
private val targetMethodName: String,
private val myCallingExpressionLines: Range<Int>
) : NamedMethodFilter {
init {
assert(declarationPtr != null || isInvoke)
}
override fun getCallingExpressionLines() = myCallingExpressionLines
override fun getMethodName() = targetMethodName
override fun locationMatches(process: DebugProcessImpl, location: Location): Boolean {
val method = location.method()
if (targetMethodName != method.name()) return false
val positionManager = process.positionManager
val (currentDescriptor, currentDeclaration) = runReadAction {
val elementAt = positionManager.getSourcePosition(location)?.elementAt
val declaration = elementAt?.getParentOfTypesAndPredicate(false, KtDeclaration::class.java) {
it !is KtProperty || !it.isLocal
}
if (declaration is KtClass && method.name() == "<init>") {
declaration.resolveToDescriptorIfAny()?.unsubstitutedPrimaryConstructor to declaration
} else {
declaration?.resolveToDescriptorIfAny() to declaration
}
}
if (currentDescriptor == null || currentDeclaration == null) {
return false
}
@Suppress("FoldInitializerAndIfToElvis")
if (currentDescriptor !is CallableMemberDescriptor) return false
if (currentDescriptor.kind != DECLARATION) return false
if (isInvoke) {
// There can be only one 'invoke' target at the moment so consider position as expected.
// Descriptors can be not-equal, say when parameter has type `(T) -> T` and lambda is `Int.() -> Int`.
return true
}
val declaration = declarationPtr?.element
?: return true // Element is lost. But we know that name is matches, so stop.
if (currentDeclaration.isEquivalentTo(declaration)) {
return true
}
return DescriptorUtils.getAllOverriddenDescriptors(currentDescriptor).any { baseOfCurrent ->
val currentBaseDeclaration = DescriptorToSourceUtilsIde.getAnyDeclaration(currentDeclaration.project, baseOfCurrent)
declaration.isEquivalentTo(currentBaseDeclaration)
}
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.BreakpointStepMethodFilter
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.util.Range
import com.sun.jdi.Location
import org.jetbrains.kotlin.codegen.coroutines.isResumeImplMethodNameFromAnyLanguageSettings
import org.jetbrains.kotlin.idea.core.util.isMultiLine
import org.jetbrains.kotlin.idea.debugger.isInsideInlineArgument
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.util.OperatorNameConventions
class KotlinLambdaMethodFilter(
private val lambda: KtFunction,
private val myCallingExpressionLines: Range<Int>,
private val isInline: Boolean,
private val isSuspend: Boolean
) : BreakpointStepMethodFilter {
private val myFirstStatementPosition: SourcePosition?
private val myLastStatementLine: Int
init {
val body = lambda.bodyExpression
if (body != null && lambda.isMultiLine()) {
var firstStatementPosition: SourcePosition? = null
var lastStatementPosition: SourcePosition? = null
val statements = (body as? KtBlockExpression)?.statements ?: listOf(body)
if (statements.isNotEmpty()) {
firstStatementPosition = SourcePosition.createFromElement(statements.first())
if (firstStatementPosition != null) {
val lastStatement = statements.last()
lastStatementPosition = SourcePosition.createFromOffset(firstStatementPosition.file, lastStatement.textRange.endOffset)
}
}
myFirstStatementPosition = firstStatementPosition
myLastStatementLine = if (lastStatementPosition != null) lastStatementPosition.line else -1
} else {
myFirstStatementPosition = SourcePosition.createFromElement(lambda)
myLastStatementLine = myFirstStatementPosition!!.line
}
}
override fun getBreakpointPosition() = myFirstStatementPosition
override fun getLastStatementLine() = myLastStatementLine
override fun locationMatches(process: DebugProcessImpl, location: Location): Boolean {
val method = location.method()
if (isInline) {
return isInsideInlineArgument(lambda, location, process)
}
return isLambdaName(method.name())
}
override fun getCallingExpressionLines() = if (isInline) Range(0, Int.MAX_VALUE) else myCallingExpressionLines
private fun isLambdaName(name: String?): Boolean {
if (isSuspend && name != null) {
return isResumeImplMethodNameFromAnyLanguageSettings(name)
}
return name == OperatorNameConventions.INVOKE.asString()
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.actions.SmartStepTarget
import com.intellij.util.Range
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.util.OperatorNameConventions
import javax.swing.Icon
class KotlinLambdaSmartStepTarget(
label: String,
highlightElement: KtFunction,
lines: Range<Int>,
val isInline: Boolean,
val isSuspend: Boolean
) : SmartStepTarget(label, highlightElement, true, lines) {
override fun getIcon(): Icon = KotlinIcons.LAMBDA
fun getLambda() = highlightElement as KtFunction
companion object {
fun calcLabel(descriptor: DeclarationDescriptor, paramName: Name): String {
return "${descriptor.name.asString()}: ${paramName.asString()}.${OperatorNameConventions.INVOKE.asString()}()"
}
}
}
@@ -0,0 +1,79 @@
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.actions.SmartStepTarget
import com.intellij.psi.PsiElement
import com.intellij.util.Range
import org.jetbrains.kotlin.builtins.functions.FunctionInvokeDescriptor
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.renderer.ParameterNameRenderingPolicy
import org.jetbrains.kotlin.renderer.PropertyAccessorRenderingPolicy
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension
import javax.swing.Icon
class KotlinMethodSmartStepTarget(
descriptor: CallableMemberDescriptor,
val declaration: KtDeclaration?,
label: String,
highlightElement: PsiElement,
lines: Range<Int>
) : SmartStepTarget(label, highlightElement, false, lines) {
val isInvoke = descriptor is FunctionInvokeDescriptor
init {
assert(declaration != null || isInvoke)
}
private val isExtension = descriptor.isExtension
val targetMethodName: String = when (descriptor) {
is ClassDescriptor, is ConstructorDescriptor -> "<init>"
is PropertyAccessorDescriptor -> JvmAbi.getterName(descriptor.correspondingProperty.name.asString())
else -> descriptor.name.asString()
}
override fun getIcon(): Icon? {
return when {
isExtension -> KotlinIcons.EXTENSION_FUNCTION
else -> KotlinIcons.FUNCTION
}
}
companion object {
private val renderer = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_NO_ANNOTATIONS.withOptions {
parameterNameRenderingPolicy = ParameterNameRenderingPolicy.NONE
withoutReturnType = true
propertyAccessorRenderingPolicy = PropertyAccessorRenderingPolicy.PRETTY
startFromName = true
modifiers = emptySet()
}
fun calcLabel(descriptor: DeclarationDescriptor): String {
return renderer.render(descriptor)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is KotlinMethodSmartStepTarget) return false
if (isInvoke && other.isInvoke) {
// Don't allow to choose several invoke targets in smart step into as we can't distinguish them reliably during debug
return true
}
return declaration === other.declaration
}
override fun hashCode(): Int {
if (isInvoke) {
// Predefined value to make all FunctionInvokeDescriptor targets equal
return 42
}
return declaration!!.hashCode()
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.SimplePropertyGetterProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.*
class KotlinSimpleGetterProvider : SimplePropertyGetterProvider {
override fun isInsideSimpleGetter(element: PsiElement): Boolean {
// class A(val a: Int)
if (element is KtParameter) {
return true
}
val accessor = PsiTreeUtil.getParentOfType(element, KtPropertyAccessor::class.java)
if (accessor != null && accessor.isGetter) {
val body = accessor.bodyExpression
return when (body) {
is KtBlockExpression -> {
// val a: Int get() { return field }
val returnedExpression = (body.statements.singleOrNull() as? KtReturnExpression)?.returnedExpression ?: return false
returnedExpression.textMatches("field")
}
is KtExpression -> body.textMatches("field") // val a: Int get() = field
else -> false
}
}
val property = PsiTreeUtil.getParentOfType(element, KtProperty::class.java)
// val a = foo()
if (property != null) {
return property.getter == null && !property.isLocal
}
return false
}
}
@@ -0,0 +1,251 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.actions.JvmSmartStepIntoHandler
import com.intellij.debugger.actions.MethodSmartStepTarget
import com.intellij.debugger.actions.SmartStepTarget
import com.intellij.debugger.engine.MethodFilter
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiMethod
import com.intellij.util.Range
import com.intellij.util.containers.OrderedSet
import org.jetbrains.kotlin.builtins.functions.FunctionInvokeDescriptor
import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType
import org.jetbrains.kotlin.builtins.isSuspendFunctionType
import org.jetbrains.kotlin.codegen.intrinsics.IntrinsicMethods
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.isFromJava
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getParentCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.inline.InlineUtil
class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() {
override fun isAvailable(position: SourcePosition?) = position?.file is KtFile
override fun findSmartStepTargets(position: SourcePosition): List<SmartStepTarget> {
val file = position.file
val elementAtOffset = position.elementAt ?: return emptyList()
val element = CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, elementAtOffset.textRange.startOffset) as? KtElement
?: return emptyList()
val elementTextRange = element.textRange ?: return emptyList()
val doc = PsiDocumentManager.getInstance(file.project).getDocument(file) ?: return emptyList()
val lines = Range(doc.getLineNumber(elementTextRange.startOffset), doc.getLineNumber(elementTextRange.endOffset))
@Suppress("DEPRECATION")
val bindingContext = element.analyzeWithAllCompilerChecks().bindingContext
val result = OrderedSet<SmartStepTarget>()
// TODO support class initializers, local functions, delegated properties with specified type, setter for properties
element.accept(object : KtTreeVisitorVoid() {
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
recordFunctionLiteral(lambdaExpression.functionLiteral)
}
override fun visitNamedFunction(function: KtNamedFunction) {
if (!recordFunctionLiteral(function)) {
super.visitNamedFunction(function)
}
}
private fun recordFunctionLiteral(function: KtFunction): Boolean {
val context = function.analyze()
val resolvedCall = function.getParentCall(context).getResolvedCall(context)
if (resolvedCall != null) {
val arguments = resolvedCall.valueArguments
for ((param, argument) in arguments) {
if (argument.arguments.any { getArgumentExpression(it) == function }) {
val resultingDescriptor = resolvedCall.resultingDescriptor
val label = KotlinLambdaSmartStepTarget.calcLabel(resultingDescriptor, param.name)
result.add(
KotlinLambdaSmartStepTarget(
label, function, lines, InlineUtil.isInline(resultingDescriptor), param.type.isSuspendFunctionType
)
)
return true
}
}
}
return false
}
private fun getArgumentExpression(it: ValueArgument) =
(it.getArgumentExpression() as? KtLambdaExpression)?.functionLiteral ?: it.getArgumentExpression()
override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) {
// skip calls in object declarations
}
override fun visitIfExpression(expression: KtIfExpression) {
expression.condition?.accept(this)
}
override fun visitWhileExpression(expression: KtWhileExpression) {
expression.condition?.accept(this)
}
override fun visitDoWhileExpression(expression: KtDoWhileExpression) {
expression.condition?.accept(this)
}
override fun visitForExpression(expression: KtForExpression) {
expression.loopRange?.accept(this)
}
override fun visitWhenExpression(expression: KtWhenExpression) {
expression.subjectExpression?.accept(this)
}
override fun visitArrayAccessExpression(expression: KtArrayAccessExpression) {
recordFunction(expression)
super.visitArrayAccessExpression(expression)
}
override fun visitUnaryExpression(expression: KtUnaryExpression) {
recordFunction(expression.operationReference)
super.visitUnaryExpression(expression)
}
override fun visitBinaryExpression(expression: KtBinaryExpression) {
recordFunction(expression.operationReference)
super.visitBinaryExpression(expression)
}
override fun visitCallExpression(expression: KtCallExpression) {
val calleeExpression = expression.calleeExpression
if (calleeExpression != null) {
recordFunction(calleeExpression)
}
super.visitCallExpression(expression)
}
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
recordGetter(expression)
super.visitSimpleNameExpression(expression)
}
private fun recordGetter(expression: KtSimpleNameExpression) {
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
val propertyDescriptor = resolvedCall.resultingDescriptor as? PropertyDescriptor ?: return
val getterDescriptor = propertyDescriptor.getter
if (getterDescriptor == null || getterDescriptor.isDefault) return
val ktDeclaration = DescriptorToSourceUtilsIde.getAnyDeclaration(file.project, getterDescriptor) as? KtDeclaration ?: return
val delegatedResolvedCall = bindingContext[BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, getterDescriptor]
if (delegatedResolvedCall != null) {
val delegatedPropertyGetterDescriptor = delegatedResolvedCall.resultingDescriptor
val label = "${propertyDescriptor.name}." + KotlinMethodSmartStepTarget.calcLabel(delegatedPropertyGetterDescriptor)
result.add(KotlinMethodSmartStepTarget(delegatedPropertyGetterDescriptor, ktDeclaration, label, expression, lines))
} else {
if (ktDeclaration is KtPropertyAccessor && ktDeclaration.hasBody()) {
val label = KotlinMethodSmartStepTarget.calcLabel(getterDescriptor)
result.add(KotlinMethodSmartStepTarget(getterDescriptor, ktDeclaration, label, expression, lines))
}
}
}
private fun recordFunction(expression: KtExpression) {
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
val descriptor = resolvedCall.resultingDescriptor
if (descriptor !is FunctionDescriptor || isIntrinsic(descriptor)) return
val declaration = DescriptorToSourceUtilsIde.getAnyDeclaration(file.project, descriptor)
if (descriptor.isFromJava) {
(declaration as? PsiMethod)?.let {
result.add(MethodSmartStepTarget(it, null, declaration, false, lines))
}
} else {
if (declaration == null && !isInvokeInBuiltinFunction(descriptor)) {
return
}
if (declaration !is KtDeclaration?) return
if (descriptor is ConstructorDescriptor && descriptor.isPrimary) {
if (declaration is KtClass && declaration.getAnonymousInitializers().isEmpty()) {
// There is no constructor or init block, so do not show it in smart step into
return
}
}
val callLabel = KotlinMethodSmartStepTarget.calcLabel(descriptor)
val label = when (descriptor) {
is FunctionInvokeDescriptor -> {
when (expression) {
is KtSimpleNameExpression -> "${runReadAction { expression.text }}.$callLabel"
else -> callLabel
}
}
else -> callLabel
}
result.add(KotlinMethodSmartStepTarget(descriptor, declaration, label, expression, lines))
}
}
}, null)
return result
}
override fun createMethodFilter(stepTarget: SmartStepTarget?): MethodFilter? {
return when (stepTarget) {
is KotlinMethodSmartStepTarget ->
KotlinBasicStepMethodFilter(
stepTarget.declaration?.createSmartPointer(),
stepTarget.isInvoke,
stepTarget.targetMethodName,
stepTarget.callingExpressionLines!!
)
is KotlinLambdaSmartStepTarget ->
KotlinLambdaMethodFilter(
stepTarget.getLambda(), stepTarget.callingExpressionLines!!, stepTarget.isInline, stepTarget.isSuspend
)
else -> super.createMethodFilter(stepTarget)
}
}
private val methods = IntrinsicMethods(JvmTarget.JVM_1_6)
private fun isIntrinsic(descriptor: CallableMemberDescriptor): Boolean {
return methods.getIntrinsic(descriptor) != null
}
private fun isInvokeInBuiltinFunction(descriptor: DeclarationDescriptor): Boolean {
if (descriptor !is FunctionInvokeDescriptor) return false
val classDescriptor = descriptor.containingDeclaration as? ClassDescriptor ?: return false
return classDescriptor.defaultType.isBuiltinFunctionalType
}
}
@@ -0,0 +1,161 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.util.EventDispatcher
import com.sun.jdi.request.EventRequest
import com.sun.jdi.request.StepRequest
import java.lang.reflect.Field
// Mass-copy-paste code for commands behaviour from com.intellij.debugger.engine.DebugProcessImpl
@SuppressWarnings("UnnecessaryFinalOnLocalVariableOrParameter")
class KotlinStepActionFactory(private val debuggerProcess: DebugProcessImpl) {
abstract class KotlinStepAction {
abstract fun contextAction(suspendContext: SuspendContextImpl)
}
fun createKotlinStepOverInlineAction(smartStepFilter: KotlinMethodFilter): KotlinStepAction {
return StepOverInlineCommand(smartStepFilter, StepRequest.STEP_LINE)
}
private val debuggerContext: DebuggerContextImpl get() = debuggerProcess.debuggerContext
private val suspendManager: SuspendManager get() = debuggerProcess.suspendManager
private val project: Project get() = debuggerProcess.project
private val session: DebuggerSession get() = debuggerProcess.session
// TODO: ask for better API
// Should be safe to use reflection as field is protected and not obfuscated
private val debugProcessDispatcher: EventDispatcher<DebugProcessListener> = getFromField("myDebugProcessDispatcher")
// TODO: ask for better API
// Get field by type as it private and obfuscated in Ultimate
private val threadBlockedMonitor: ThreadBlockedMonitor = getFromField(ThreadBlockedMonitor::class.java)
private fun showStatusText(message: String) {
debuggerProcess.showStatusText(message)
}
// TODO: ask for better API
// Should be safe to use reflection as method is protected and not obfuscated
private fun doStep(
suspendContext: SuspendContextImpl,
stepThread: ThreadReferenceProxyImpl,
size: Int, depth: Int, hint: RequestHint
) {
val doStepMethod = DebugProcessImpl::class.java.getDeclaredMethod(
"doStep",
SuspendContextImpl::class.java, ThreadReferenceProxyImpl::class.java,
Integer.TYPE, Integer.TYPE, RequestHint::class.java
)
doStepMethod.isAccessible = true
doStepMethod.invoke(debuggerProcess, suspendContext, stepThread, size, depth, hint)
}
private fun <T> getFromField(fieldType: Class<T>): T {
return getFromField(DebugProcessImpl::class.java.declaredFields.single { it.type == fieldType })
}
private fun <T> getFromField(fieldName: String): T {
return getFromField(DebugProcessImpl::class.java.getDeclaredField(fieldName))
}
private fun <T> getFromField(field: Field?): T {
field!!.isAccessible = true
@Suppress("UNCHECKED_CAST")
return field.get(debuggerProcess) as T
}
private inner class StepOverInlineCommand(private val mySmartStepFilter: KotlinMethodFilter, private val myStepSize: Int) :
KotlinStepAction() {
private fun getContextThread(suspendContext: SuspendContextImpl): ThreadReferenceProxyImpl? {
val contextThread = debuggerContext.threadProxy
return contextThread ?: suspendContext.thread
}
// See: ResumeCommand.applyThreadFilter()
private fun applyThreadFilter(suspendContext: SuspendContextImpl, thread: ThreadReferenceProxyImpl) {
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
// there could be explicit resume as a result of call to voteSuspend()
// e.g. when breakpoint was considered invalid, in that case the filter will be applied _after_
// resuming and all breakpoints in other threads will be ignored.
// As resume() implicitly cleares the filter, the filter must be always applied _before_ any resume() action happens
val breakpointManager = DebuggerManagerEx.getInstanceEx(project).breakpointManager
breakpointManager.applyThreadFilter(debuggerProcess, thread.threadReference)
}
}
// See: StepCommand.resumeAction()
private fun resumeAction(suspendContext: SuspendContextImpl, thread: ThreadReferenceProxyImpl) {
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD || isResumeOnlyCurrentThread) {
threadBlockedMonitor.startWatching(thread)
}
if (isResumeOnlyCurrentThread && suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
suspendManager.resumeThread(suspendContext, thread)
} else {
suspendManager.resume(suspendContext)
}
}
// See: StepIntoCommand.contextAction()
override fun contextAction(suspendContext: SuspendContextImpl) {
showStatusText("Stepping over inline")
val stepThread = getContextThread(suspendContext)
if (stepThread == null) {
// TODO: Intellij code doesn't bother to check thread for null, so probably it's not-null actually
debuggerProcess.createStepOverCommand(suspendContext, true).contextAction(suspendContext)
return
}
val hint = KotlinStepOverInlinedLinesHint(stepThread, suspendContext, mySmartStepFilter)
hint.isResetIgnoreFilters = !session.shouldIgnoreSteppingFilters()
try {
session.setIgnoreStepFiltersFlag(stepThread.frameCount())
} catch (e: EvaluateException) {
LOG.info(e)
}
applyThreadFilter(suspendContext, stepThread)
doStep(suspendContext, stepThread, myStepSize, StepRequest.STEP_OVER, hint)
showStatusText("Process resumed")
resumeAction(suspendContext, stepThread)
debugProcessDispatcher.multicaster.resumed(suspendContext)
}
}
companion object {
private val LOG = Logger.getInstance(KotlinStepActionFactory::class.java)
private val isResumeOnlyCurrentThread: Boolean
get() = DebuggerSettings.getInstance().RESUME_ONLY_CURRENT_THREAD
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.openapi.project.Project
import com.intellij.util.Range
import com.sun.jdi.LocalVariable
import com.sun.jdi.Location
import org.jetbrains.kotlin.idea.debugger.ktLocationInfo
class StepOverFilterData(
val lineNumber: Int,
val stepOverLines: Set<Int>,
val inlineRangeVariables: List<LocalVariable>,
val isDexDebug: Boolean,
val skipAfterCodeIndex: Long = -1
)
class KotlinStepOverInlineFilter(val project: Project, val data: StepOverFilterData) : KotlinMethodFilter {
private fun Location.ktLineNumber() = ktLocationInfo(this, data.isDexDebug, project).first
override fun locationMatches(context: SuspendContextImpl, location: Location): Boolean {
val frameProxy = context.frameProxy ?: return true
if (data.skipAfterCodeIndex != -1L && location.codeIndex() > data.skipAfterCodeIndex) {
return false
}
val currentLine = location.ktLineNumber()
if (!(data.stepOverLines.contains(currentLine))) {
return currentLine != data.lineNumber
}
val visibleInlineVariables = getInlineRangeLocalVariables(frameProxy)
// Our ranges check missed exit from inline function. This is when breakpoint was in last statement of inline functions.
// This can be observed by inline local range-variables. Absence of any means step out was done.
return data.inlineRangeVariables.any { !visibleInlineVariables.contains(it) }
}
override fun locationMatches(process: DebugProcessImpl, location: Location): Boolean {
throw IllegalStateException() // Should not be called from Kotlin hint
}
override fun getCallingExpressionLines(): Range<Int>? = null
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.RequestHint
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.openapi.diagnostic.Logger
import com.sun.jdi.VMDisconnectedException
import com.sun.jdi.request.StepRequest
// Originally copied from RequestHint
class KotlinStepOverInlinedLinesHint(
stepThread: ThreadReferenceProxyImpl,
suspendContext: SuspendContextImpl,
methodFilter: KotlinMethodFilter
) : RequestHint(stepThread, suspendContext, methodFilter) {
private val LOG = Logger.getInstance(KotlinStepOverInlinedLinesHint::class.java)
private val filter = methodFilter
override fun getDepth(): Int = StepRequest.STEP_OVER
override fun getNextStepDepth(context: SuspendContextImpl): Int {
try {
val frameProxy = context.frameProxy
if (frameProxy != null) {
if (isTheSameFrame(context)) {
return if (filter.locationMatches(context, frameProxy.location())) {
STOP
} else {
StepRequest.STEP_OVER
}
}
if (isSteppedOut) {
return STOP
}
return StepRequest.STEP_OUT
}
} catch (ignored: VMDisconnectedException) {
} catch (e: EvaluateException) {
LOG.error(e)
}
return STOP
}
}
@@ -0,0 +1,599 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.NoDataException
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.MethodFilter
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.JvmSteppingCommandProvider
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.psi.PsiElement
import com.intellij.xdebugger.impl.XSourcePositionImpl
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.LocalVariable
import com.sun.jdi.Location
import com.sun.jdi.Method
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.builtins.isFunctionType
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.GOTO
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.MOVE
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN_OBJECT
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN_VOID
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN_WIDE
import org.jetbrains.kotlin.idea.core.util.getLineNumber
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.keysToMap
import kotlin.math.max
import kotlin.math.min
class KotlinSteppingCommandProvider : JvmSteppingCommandProvider() {
override fun getStepOverCommand(
suspendContext: SuspendContextImpl?,
ignoreBreakpoints: Boolean,
stepSize: Int
): DebugProcessImpl.ResumeCommand? {
if (suspendContext == null || suspendContext.isResumed) return null
val sourcePosition = suspendContext.debugProcess.debuggerContext.sourcePosition ?: return null
return getStepOverCommand(suspendContext, ignoreBreakpoints, sourcePosition)
}
@TestOnly
fun getStepOverCommand(
suspendContext: SuspendContextImpl,
ignoreBreakpoints: Boolean,
debuggerContext: DebuggerContextImpl
): DebugProcessImpl.ResumeCommand? {
return getStepOverCommand(suspendContext, ignoreBreakpoints, debuggerContext.sourcePosition)
}
private fun getStepOverCommand(
suspendContext: SuspendContextImpl,
ignoreBreakpoints: Boolean,
sourcePosition: SourcePosition
): DebugProcessImpl.ResumeCommand? {
val kotlinSourcePosition = KotlinSourcePosition.create(sourcePosition) ?: return null
if (isSpecialStepOverNeeded(kotlinSourcePosition)) {
return DebuggerSteppingHelper.createStepOverCommand(suspendContext, ignoreBreakpoints, kotlinSourcePosition)
}
val file = sourcePosition.elementAt.containingFile
val location = suspendContext.debugProcess.invokeInManagerThread { suspendContext.frameProxy?.safeLocation() } ?: return null
if (isInSuspendMethod(location) && !isOnSuspendReturnOrReenter(location) && !isLastLineLocationInMethod(location)) {
return DebugProcessImplHelper.createStepOverCommandWithCustomFilter(
suspendContext, ignoreBreakpoints, KotlinSuspendCallStepOverFilter(sourcePosition.line, file, ignoreBreakpoints)
)
}
return null
}
data class KotlinSourcePosition(
val file: KtFile, val function: KtNamedFunction,
val linesRange: IntRange, val sourcePosition: SourcePosition
) {
companion object {
fun create(sourcePosition: SourcePosition): KotlinSourcePosition? {
val file = sourcePosition.file as? KtFile ?: return null
if (sourcePosition.line < 0) return null
val elementAt = sourcePosition.elementAt ?: return null
val containingFunction = elementAt.parents
.filterIsInstance<KtNamedFunction>()
.firstOrNull { !it.isLocal } ?: return null
val startLineNumber = containingFunction.getLineNumber(true) + 1
val endLineNumber = containingFunction.getLineNumber(false) + 1
if (startLineNumber > endLineNumber) return null
val linesRange = startLineNumber..endLineNumber
return KotlinSourcePosition(file, containingFunction, linesRange, sourcePosition)
}
}
}
private fun isSpecialStepOverNeeded(kotlinSourcePosition: KotlinSourcePosition): Boolean {
val sourcePosition = kotlinSourcePosition.sourcePosition
val hasInlineCallsOnLine = getInlineFunctionCallsIfAny(sourcePosition).isNotEmpty()
if (hasInlineCallsOnLine) {
return true
}
// Step over calls to lambda arguments in inline function while execution is already in that function
val containingFunctionDescriptor = kotlinSourcePosition.function.unsafeResolveToDescriptor()
if (InlineUtil.isInline(containingFunctionDescriptor)) {
val inlineArgumentsCallsIfAny = getInlineArgumentsCallsIfAny(sourcePosition, containingFunctionDescriptor)
if (inlineArgumentsCallsIfAny != null && inlineArgumentsCallsIfAny.isNotEmpty()) {
return true
}
}
return false
}
@TestOnly
fun getStepOutCommand(suspendContext: SuspendContextImpl, debugContext: DebuggerContextImpl): DebugProcessImpl.ResumeCommand? {
return getStepOutCommand(suspendContext, debugContext.sourcePosition)
}
override fun getStepOutCommand(suspendContext: SuspendContextImpl?, stepSize: Int): DebugProcessImpl.ResumeCommand? {
if (suspendContext == null || suspendContext.isResumed) return null
val sourcePosition = suspendContext.debugProcess.debuggerContext.sourcePosition ?: return null
return getStepOutCommand(suspendContext, sourcePosition)
}
private fun getStepOutCommand(suspendContext: SuspendContextImpl, sourcePosition: SourcePosition): DebugProcessImpl.ResumeCommand? {
val file = sourcePosition.file as? KtFile ?: return null
if (sourcePosition.line < 0) return null
val lineStartOffset = file.getLineStartOffset(sourcePosition.line) ?: return null
val inlineFunctions = getInlineFunctionsIfAny(file, lineStartOffset)
val inlinedArgument = getInlineArgumentIfAny(sourcePosition.elementAt)
if (inlineFunctions.isEmpty() && inlinedArgument == null) return null
return DebuggerSteppingHelper.createStepOutCommand(suspendContext, true, inlineFunctions, inlinedArgument)
}
}
private fun PsiElement?.contains(element: PsiElement): Boolean {
return this?.textRange?.contains(element.textRange) ?: false
}
private fun getInlineCallFunctionArgumentsIfAny(sourcePosition: SourcePosition): List<KtFunction> {
val inlineFunctionCalls = getInlineFunctionCallsIfAny(sourcePosition)
return getInlineArgumentsIfAny(inlineFunctionCalls)
}
private fun getInlineFunctionsIfAny(file: KtFile, offset: Int): List<KtNamedFunction> {
val elementAt = file.findElementAt(offset) ?: return emptyList()
val containingFunction = elementAt.getParentOfType<KtNamedFunction>(false) ?: return emptyList()
val descriptor = containingFunction.unsafeResolveToDescriptor()
if (!InlineUtil.isInline(descriptor)) return emptyList()
return DebuggerUtils.analyzeElementWithInline(containingFunction, false).filterIsInstance<KtNamedFunction>()
}
private fun getInlineArgumentsIfAny(inlineFunctionCalls: List<KtCallExpression>): List<KtFunction> {
return inlineFunctionCalls.flatMap {
it.valueArguments
.map(::getArgumentExpression)
.filterIsInstance<KtFunction>()
}
}
private fun getArgumentExpression(it: ValueArgument) =
(it.getArgumentExpression() as? KtLambdaExpression)?.functionLiteral ?: it.getArgumentExpression()
private fun getInlineArgumentsCallsIfAny(
sourcePosition: SourcePosition,
declarationDescriptor: DeclarationDescriptor
): List<KtCallExpression>? {
if (declarationDescriptor !is CallableDescriptor) return null
val valueParameters = declarationDescriptor.valueParameters.filter { it.type.isFunctionType }.toSet()
if (valueParameters.isEmpty()) {
return null
}
fun isCallOfArgument(ktCallExpression: KtCallExpression): Boolean {
val resolvedCall = ktCallExpression.resolveToCall() as? VariableAsFunctionResolvedCall ?: return false
val candidateDescriptor = resolvedCall.variableCall.candidateDescriptor
return candidateDescriptor in valueParameters
}
return findCallsOnPosition(sourcePosition, ::isCallOfArgument)
}
private fun getInlineFunctionCallsIfAny(sourcePosition: SourcePosition): List<KtCallExpression> {
fun isInlineCall(expr: KtCallExpression): Boolean {
val resolvedCall = expr.resolveToCall() ?: return false
return InlineUtil.isInline(resolvedCall.resultingDescriptor)
}
return findCallsOnPosition(sourcePosition, ::isInlineCall)
}
private fun findCallsOnPosition(sourcePosition: SourcePosition, filter: (KtCallExpression) -> Boolean): List<KtCallExpression> {
val file = sourcePosition.file as? KtFile ?: return emptyList()
val lineNumber = sourcePosition.line
val lineElement = findElementAtLine(file, lineNumber)
if (lineElement !is KtElement) {
if (lineElement != null) {
val call = findCallByEndToken(lineElement)
if (call != null && filter(call)) {
return listOf(call)
}
}
return emptyList()
}
val start = lineElement.startOffset
val end = lineElement.endOffset
val allFilteredCalls = CodeInsightUtils.findElementsOfClassInRange(file, start, end, KtExpression::class.java)
.map { KtPsiUtil.getParentCallIfPresent(it as KtExpression) }
.filterIsInstance<KtCallExpression>()
.filter { filter(it) }
.toSet()
// It is necessary to check range because of multiline assign
var linesRange = lineNumber..lineNumber
return allFilteredCalls.filter {
val shouldInclude = it.getLineNumber() in linesRange
if (shouldInclude) {
linesRange = min(linesRange.start, it.getLineNumber())..max(linesRange.endInclusive, it.getLineNumber(false))
}
shouldInclude
}
}
sealed class Action(
val position: XSourcePositionImpl? = null,
val stepOverInlineData: StepOverFilterData? = null
) {
class STEP_OVER : Action() {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) =
debugProcess.createStepOverCommand(suspendContext, ignoreBreakpoints).contextAction(suspendContext)
}
class STEP_OUT : Action() {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) =
debugProcess.createStepOutCommand(suspendContext).contextAction(suspendContext)
}
class RUN_TO_CURSOR(position: XSourcePositionImpl) : Action(position) {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) {
return runReadAction {
debugProcess.createRunToCursorCommand(suspendContext, position!!, ignoreBreakpoints)
}.contextAction(suspendContext)
}
}
class STEP_OVER_INLINED(stepOverInlineData: StepOverFilterData) : Action(stepOverInlineData = stepOverInlineData) {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) {
return KotlinStepActionFactory(debugProcess).createKotlinStepOverInlineAction(
KotlinStepOverInlineFilter(debugProcess.project, stepOverInlineData!!)
).contextAction(suspendContext)
}
}
abstract fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean)
}
interface KotlinMethodFilter : MethodFilter {
fun locationMatches(context: SuspendContextImpl, location: Location): Boolean
}
fun getStepOverAction(
location: Location,
kotlinSourcePosition: KotlinSteppingCommandProvider.KotlinSourcePosition,
frameProxy: StackFrameProxyImpl,
isDexDebug: Boolean
): Action {
val inlineArgumentsToSkip = runReadAction {
getInlineCallFunctionArgumentsIfAny(kotlinSourcePosition.sourcePosition)
}
return getStepOverAction(
location, kotlinSourcePosition.file, kotlinSourcePosition.linesRange,
inlineArgumentsToSkip, frameProxy, isDexDebug
)
}
fun getStepOverAction(
location: Location,
sourceFile: KtFile,
range: IntRange,
inlineFunctionArguments: List<KtElement>,
frameProxy: StackFrameProxyImpl,
isDexDebug: Boolean
): Action {
location.declaringType() ?: return Action.STEP_OVER()
val project = sourceFile.project
val methodLocations = location.method().safeAllLineLocations()
if (methodLocations.isEmpty()) {
return Action.STEP_OVER()
}
val locationsLineAndFile = methodLocations.keysToMap { ktLocationInfo(it, isDexDebug, project, true) }
fun Location.ktLineNumber(): Int = (locationsLineAndFile[this] ?: ktLocationInfo(this, isDexDebug, project, true)).first
fun Location.ktFileName(): String {
val ktFile = (locationsLineAndFile[this] ?: ktLocationInfo(this, isDexDebug, project, true)).second
// File is not null only for inlined locations. Get file name from debugger information otherwise.
return ktFile?.name ?: this.sourceName(KOTLIN_STRATA_NAME)
}
fun isThisMethodLocation(nextLocation: Location): Boolean {
if (nextLocation.method() != location.method()) {
return false
}
val ktLineNumber = nextLocation.ktLineNumber()
if (ktLineNumber !in range) {
return false
}
try {
return nextLocation.ktFileName() == sourceFile.name
} catch (e: AbsentInformationException) {
return true
}
}
fun isBackEdgeLocation(): Boolean {
val previousSuitableLocation = methodLocations.reversed()
.dropWhile { it != location }
.drop(1)
.filter(::isThisMethodLocation)
.dropWhile { it.ktLineNumber() == location.ktLineNumber() }
.firstOrNull()
return previousSuitableLocation != null && previousSuitableLocation.ktLineNumber() > location.ktLineNumber()
}
val patchedLocation = if (isBackEdgeLocation()) {
// Pretend we had already done a backing step
methodLocations
.filter(::isThisMethodLocation)
.firstOrNull { it.ktLineNumber() == location.ktLineNumber() } ?: location
} else {
location
}
val patchedLineNumber = patchedLocation.ktLineNumber()
val lambdaArgumentRanges = runReadAction {
inlineFunctionArguments.map {
val startLineNumber = it.getLineNumber(true) + 1
val endLineNumber = it.getLineNumber(false) + 1
startLineNumber..endLineNumber
}
}
val inlineRangeVariables = getInlineRangeLocalVariables(frameProxy)
// Try to find the range for step over:
// - Lines from other files and from functions that are not in range of current one are definitely inlined and should be stepped over.
// - Lines in function arguments of inlined functions are inlined too as we found them starting from the position of inlined call.
// - Current line locations should also be stepped over.
//
// We might erroneously extend this range too much when there's a call of function argument or other
// inline function in last statement of inline function. The list of inlineRangeVariables will be used later to overcome it.
val stepOverLocations = methodLocations
.dropWhile { it != patchedLocation }
.drop(1)
.dropWhile { it.ktLineNumber() == patchedLineNumber }
.takeWhile { loc ->
!isThisMethodLocation(loc) || lambdaArgumentRanges.any { loc.ktLineNumber() in it } || loc.ktLineNumber() == patchedLineNumber
}
if (!stepOverLocations.isEmpty()) {
// Some Kotlin inlined methods with 'for' (and maybe others) generates bytecode that after dexing have a strange artifact.
// GOTO instructions are moved to the end of method and as they don't have proper line, line is obtained from the previous
// instruction. It might be method return or previous GOTO from the inlining. Simple stepping over such function is really
// terrible. On each iteration position jumps to the method end or some previous inline call and then returns back. To prevent
// this filter locations with too big code indexes manually
val returnCodeIndex: Long = if (isDexDebug) {
val method = location.method()
val locationsOfLine = method.safeLocationsOfLine(range.last)
if (locationsOfLine.isNotEmpty()) {
locationsOfLine.map { it.codeIndex() }.max() ?: -1L
} else {
findReturnFromDexBytecode(location.method())
}
} else -1L
return Action.STEP_OVER_INLINED(
StepOverFilterData(
patchedLineNumber,
stepOverLocations.map { it.ktLineNumber() }.toSet(),
inlineRangeVariables,
isDexDebug,
returnCodeIndex
)
)
}
return Action.STEP_OVER()
}
fun getStepOutAction(
location: Location,
suspendContext: SuspendContextImpl,
inlineFunctions: List<KtNamedFunction>,
inlinedArgument: KtFunctionLiteral?
): Action {
val computedReferenceType = location.declaringType() ?: return Action.STEP_OUT()
val locations = computedReferenceType.safeAllLineLocations()
val nextLineLocations = locations
.dropWhile { it != location }
.drop(1)
.filter { it.method() == location.method() }
.dropWhile { it.lineNumber() == location.lineNumber() }
if (inlineFunctions.isNotEmpty()) {
val position = suspendContext.getXPositionForStepOutFromInlineFunction(nextLineLocations, inlineFunctions)
return position?.let { Action.RUN_TO_CURSOR(it) } ?: Action.STEP_OVER()
}
if (inlinedArgument != null) {
val position = suspendContext.getXPositionForStepOutFromInlinedArgument(nextLineLocations, inlinedArgument)
return position?.let { Action.RUN_TO_CURSOR(it) } ?: Action.STEP_OVER()
}
return Action.STEP_OVER()
}
private fun SuspendContextImpl.getXPositionForStepOutFromInlineFunction(
locations: List<Location>,
inlineFunctionsToSkip: List<KtNamedFunction>
): XSourcePositionImpl? {
return getNextPositionWithFilter(locations) { offset, elementAt ->
if (inlineFunctionsToSkip.any { it.textRange.contains(offset) }) {
return@getNextPositionWithFilter true
}
getInlineArgumentIfAny(elementAt) != null
}
}
private fun SuspendContextImpl.getXPositionForStepOutFromInlinedArgument(
locations: List<Location>,
inlinedArgumentToSkip: KtFunctionLiteral
): XSourcePositionImpl? {
return getNextPositionWithFilter(locations) { offset, _ ->
inlinedArgumentToSkip.textRange.contains(offset)
}
}
private fun SuspendContextImpl.getNextPositionWithFilter(
locations: List<Location>,
skip: (Int, PsiElement) -> Boolean
): XSourcePositionImpl? {
for (location in locations) {
val position = runReadAction l@{
val sourcePosition = try {
this.debugProcess.positionManager.getSourcePosition(location)
} catch (e: NoDataException) {
null
} ?: return@l null
val file = sourcePosition.file as? KtFile ?: return@l null
val elementAt = sourcePosition.elementAt ?: return@l null
val currentLine = location.lineNumber() - 1
val lineStartOffset = file.getLineStartOffset(currentLine) ?: return@l null
if (skip(lineStartOffset, elementAt)) return@l null
XSourcePositionImpl.createByElement(elementAt)
}
if (position != null) {
return position
}
}
return null
}
fun getInlineRangeLocalVariables(stackFrame: StackFrameProxyImpl): List<LocalVariable> {
return stackFrame.safeVisibleVariables()
.filter {
val name = it.name()
name.startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION)
}
.map { it.variable }
}
private fun getInlineArgumentIfAny(elementAt: PsiElement?): KtFunctionLiteral? {
val functionLiteralExpression = elementAt?.getParentOfType<KtLambdaExpression>(false) ?: return null
val context = functionLiteralExpression.analyze(BodyResolveMode.PARTIAL)
if (!InlineUtil.isInlinedArgument(functionLiteralExpression.functionLiteral, context, false)) return null
return functionLiteralExpression.functionLiteral
}
private fun findReturnFromDexBytecode(method: Method): Long {
val methodLocations = method.safeAllLineLocations()
if (methodLocations.isEmpty()) {
return -1L
}
var lastMethodCodeIndex = methodLocations.last().codeIndex()
// Continue while it's possible to get location
while (true) {
if (method.locationOfCodeIndex(lastMethodCodeIndex + 1) != null) {
lastMethodCodeIndex++
} else {
break
}
}
var returnIndex = lastMethodCodeIndex + 1
val bytecode = method.bytecodes()
var i = bytecode.size
while (i >= 2) {
// Can step only through two-byte instructions and abort on any unknown one
i -= 2
returnIndex -= 1
val instruction = bytecode[i].toInt()
if (instruction == RETURN_VOID || instruction == RETURN || instruction == RETURN_WIDE || instruction == RETURN_OBJECT) {
// Instruction found
return returnIndex
} else if (instruction == MOVE || instruction == GOTO) {
// proceed
} else {
// Don't know the instruction and it's length. Abort.
break
}
}
return -1L
}
object DexBytecode {
val RETURN_VOID = 0x0e
val RETURN = 0x0f
val RETURN_WIDE = 0x10
val RETURN_OBJECT = 0x11
val GOTO = 0x28
val MOVE = 0x01
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingConfigurableUi">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="99d36" class="javax.swing.JCheckBox" binding="ignoreKotlinMethods">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="false"/>
<text resource-bundle="org/jetbrains/kotlin/idea/KotlinBundle" key="debugger.filter.ignore.internal.classes"/>
</properties>
</component>
<vspacer id="c37da">
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping;
import com.intellij.openapi.options.ConfigurableUi;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings;
import javax.swing.*;
public class KotlinSteppingConfigurableUi implements ConfigurableUi<KotlinDebuggerSettings> {
private JCheckBox ignoreKotlinMethods;
private JPanel myPanel;
@Override
public void reset(@NotNull KotlinDebuggerSettings settings) {
boolean flag = settings.getDEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES();
ignoreKotlinMethods.setSelected(flag);
}
@Override
public boolean isModified(@NotNull KotlinDebuggerSettings settings) {
return settings.getDEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES() != ignoreKotlinMethods.isSelected();
}
@Override
public void apply(@NotNull KotlinDebuggerSettings settings) {
settings.setDEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES(ignoreKotlinMethods.isSelected());
}
@NotNull
@Override
public JComponent getComponent() {
return myPanel;
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.MethodFilter
import com.intellij.debugger.engine.RequestHint
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.psi.PsiFile
import com.intellij.util.Range
import com.intellij.xdebugger.impl.XSourcePositionImpl
import com.sun.jdi.Location
import com.sun.jdi.request.EventRequest
import org.jetbrains.kotlin.idea.debugger.isOnSuspendReturnOrReenter
import org.jetbrains.kotlin.idea.debugger.suspendFunctionFirstLineLocation
import org.jetbrains.kotlin.idea.util.application.runReadAction
class KotlinSuspendCallStepOverFilter(
private val line: Int,
private val file: PsiFile,
private val ignoreBreakpoints: Boolean
) : MethodFilter {
override fun getCallingExpressionLines(): Range<Int>? = Range(line, line)
override fun locationMatches(process: DebugProcessImpl, location: Location?): Boolean {
return location != null && isOnSuspendReturnOrReenter(location)
}
override fun onReached(context: SuspendContextImpl, hint: RequestHint): Int {
val location = context.frameProxy?.location() ?: return RequestHint.STOP
val suspendStartLineNumber = suspendFunctionFirstLineLocation(location) ?: return RequestHint.STOP
val debugProcess = context.debugProcess
val breakpointManager = DebuggerManagerEx.getInstanceEx(debugProcess.project).breakpointManager
breakpointManager.applyThreadFilter(debugProcess, null)
createRunToCursorBreakpoint(context, suspendStartLineNumber - 1, file, ignoreBreakpoints)
return RequestHint.RESUME
}
}
private fun createRunToCursorBreakpoint(context: SuspendContextImpl, line: Int, file: PsiFile, ignoreBreakpoints: Boolean) {
val position = XSourcePositionImpl.create(file.virtualFile, line) ?: return
val process = context.debugProcess
process.showStatusText(DebuggerBundle.message("status.run.to.cursor"))
process.cancelRunToCursorBreakpoint()
if (ignoreBreakpoints) {
DebuggerManagerEx.getInstanceEx(process.project).breakpointManager.disableBreakpoints(process)
}
val runToCursorBreakpoint =
runReadAction {
DebuggerManagerEx.getInstanceEx(process.project).breakpointManager.addRunToCursorBreakpoint(position, ignoreBreakpoints)
} ?: return
runToCursorBreakpoint.suspendPolicy = when {
context.suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD -> DebuggerSettings.SUSPEND_THREAD
else -> DebuggerSettings.SUSPEND_ALL
}
runToCursorBreakpoint.createRequest(process)
process.setRunToCursorBreakpoint(runToCursorBreakpoint)
}
@@ -0,0 +1,92 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.BreakpointStepMethodFilter
import com.intellij.debugger.engine.MethodFilter
import com.intellij.debugger.engine.RequestHint
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.openapi.diagnostic.Logger
import com.sun.jdi.VMDisconnectedException
import com.sun.jdi.request.StepRequest
import org.intellij.lang.annotations.MagicConstant
import java.lang.reflect.Field
internal class RequestHintWithMethodFilter(
stepThread: ThreadReferenceProxyImpl,
suspendContext: SuspendContextImpl,
@MagicConstant(
intValues = longArrayOf(
StepRequest.STEP_INTO.toLong(),
StepRequest.STEP_OVER.toLong(),
StepRequest.STEP_OUT.toLong()
)
) depth: Int,
methodFilter: MethodFilter
) : RequestHint(stepThread, suspendContext, methodFilter) {
private var targetMethodMatched = false
init {
// NOTE: Debugger API. Open RequestHint constructor with depth
if (depth != StepRequest.STEP_INTO) {
findFieldWithValue(StepRequest.STEP_INTO, Integer.TYPE)?.setInt(this, depth)
}
}
private fun findFieldWithValue(value: Int, type: Class<*>): Field? {
return RequestHint::class.java.declaredFields.firstOrNull { field ->
if (field.type == type) {
field.isAccessible = true
if (field.getInt(this) == value) {
return@firstOrNull true
}
}
false
}
}
override fun getNextStepDepth(context: SuspendContextImpl): Int {
try {
val frameProxy = context.frameProxy
val filter = methodFilter
if (filter != null && frameProxy != null && filter !is BreakpointStepMethodFilter) {
/*NODE: Debugger API. Base implementation works only for smart step into, and calls filter only if !isTheSameFrame(context). */
if (filter.locationMatches(context.debugProcess, frameProxy.location())) {
targetMethodMatched = true
return filter.onReached(context, this)
}
}
} catch (ignored: VMDisconnectedException) {
return STOP
} catch (e: EvaluateException) {
LOG.error(e)
return STOP
}
return super.getNextStepDepth(context)
}
override fun wasStepTargetMethodMatched(): Boolean {
return super.wasStepTargetMethodMatched() || targetMethodMatched
}
}
private val LOG = Logger.getInstance(RequestHintWithMethodFilter::class.java)