Evaluator: New implementation for variable finder

Support both old and new receiver variable conventions in variable finder.
This commit is contained in:
Yan Zhulanow
2018-12-07 19:56:30 +09:00
parent df7bbf707b
commit 5c419fc629
13 changed files with 514 additions and 439 deletions
@@ -7,12 +7,8 @@ package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.evaluation.AbsentInformationEvaluateException
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.psi.PsiElement
import com.sun.jdi.*
import com.sun.tools.jdi.LocalVariableImpl
@@ -21,6 +17,7 @@ import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousCl
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.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
@@ -33,95 +30,18 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.org.objectweb.asm.Type as AsmType
import java.util.*
fun StackFrameProxyImpl.visibleVariablesSafe(): List<LocalVariableProxyImpl> {
try {
return visibleVariables()
} catch (e: AbsentInformationEvaluateException) {
// Current implementation of visibleVariables() wraps an AbsentInformationException into EvaluateException
return emptyList()
} catch (e: AbsentInformationException) {
return emptyList()
fun calculateInlineDepth(variableName: String): Int {
var lastIndex = variableName.lastIndex
var depth = 0
while (variableName.lastIndexOf(INLINE_FUN_VAR_SUFFIX, startIndex = lastIndex) >= 0) {
lastIndex -= INLINE_FUN_VAR_SUFFIX.length
depth++
}
}
fun Method.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun ReferenceType.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun Method.safeLocationsOfLine(line: Int): List<Location> {
return try {
locationsOfLine(line)
} catch (e: AbsentInformationException) {
emptyList()
}
}
fun Method.safeVariables(): List<LocalVariable>? {
return try {
variables()
} catch (e: AbsentInformationException) {
null
}
}
fun Method.safeArguments(): List<LocalVariable>? {
return try {
arguments()
} catch (e: AbsentInformationException) {
null
}
}
fun Method.safeReturnType(): Type? {
return try {
returnType()
} catch (e: ClassNotLoadedException) {
null
}
}
fun LocalVariable.safeType(): Type? {
return try {
return type()
} catch (e: ClassNotLoadedException) {
null
}
}
fun Location.safeSourceName(): String? {
return try {
sourceName()
} catch (e: AbsentInformationException) {
null
} catch (e: InternalError) {
null
}
}
fun Location.safeLineNumber(): Int {
return DebuggerUtilsEx.getLineNumber(this, false)
}
fun Location.safeSourceLineNumber(): Int {
return DebuggerUtilsEx.getLineNumber(this, true)
}
fun Location.safeMethod(): Method? {
return DebuggerUtilsEx.getMethod(this)
}
fun isInsideInlineFunctionBody(visibleVariables: List<LocalVariable>): Boolean {
return visibleVariables.any { it.name().startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION) }
}
fun numberOfInlinedFunctions(visibleVariables: List<LocalVariable>): Int {
return visibleVariables.count { it.name().startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION) }
return depth
}
fun isInsideInlineArgument(
@@ -340,4 +260,32 @@ fun PropertyDescriptor.getBackingFieldName(): String? {
val jvmNameAnnotation = DescriptorUtils.findJvmNameAnnotation(this) ?: return name.asString()
return jvmNameAnnotation.allValueArguments.values.singleOrNull()?.toString()
}
}
fun Type.isSubtype(className: String): Boolean = isSubtype(AsmType.getObjectType(className))
fun Type.isSubtype(type: AsmType): Boolean {
if (this.signature() == type.descriptor) {
return true
}
if (type.sort != AsmType.OBJECT || this !is ClassType) {
return false
}
val superTypeName = type.className
if (allInterfaces().any { it.name() == superTypeName }) {
return true
}
var superClass = superclass()
while (superClass != null) {
if (superClass.name() == superTypeName) {
return true
}
superClass = superClass.superclass()
}
return false
}
@@ -1,322 +0,0 @@
/*
* 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.evaluate
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.openapi.diagnostic.Attachment
import com.sun.jdi.ClassType
import com.sun.jdi.InvalidStackFrameException
import com.sun.jdi.ObjectReference
import com.sun.jdi.StackFrame
import org.jetbrains.eval4j.Value
import org.jetbrains.eval4j.jdi.asJdiValue
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.eval4j.obj
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.inline.CAPTURED_FIELD_PREFIX
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.codegen.inline.INLINE_TRANSFORMATION_SUFFIX
import org.jetbrains.kotlin.codegen.inline.NUMBERED_FUNCTION_PREFIX
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils
import org.jetbrains.kotlin.idea.debugger.isInsideInlineFunctionBody
import org.jetbrains.kotlin.idea.debugger.numberOfInlinedFunctions
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.attachment.mergeAttachments
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.Type
class FrameVisitor(val context: EvaluationContextImpl) {
private val scope = context.debugProcess.searchScope
private val frame = context.frameProxy
companion object {
val OBJECT_TYPE = Type.getType(Any::class.java)
}
fun findValue(name: String, asmType: Type?, checkType: Boolean, failIfNotFound: Boolean): Value? {
val frame = this.frame?.stackFrame ?: return null
try {
when (name) {
THIS_NAME -> {
val thisValue = findThis(frame, asmType)
if (thisValue != null) {
return thisValue
}
}
else -> {
if (isInsideInlineFunctionBody(frame.visibleVariables())) {
val number = numberOfInlinedFunctions(frame.visibleVariables())
for (inlineFunctionIndex in number downTo 1) {
val inlineFunVar = findLocalVariableForInlineArgument(name, inlineFunctionIndex, asmType, true)
if (inlineFunVar != null) {
return inlineFunVar
}
}
}
if (isFunctionType(asmType)) {
val variableForLocalFun = findLocalVariableForLocalFun(name, asmType, checkType)
if (variableForLocalFun != null) {
return variableForLocalFun
}
}
val localVariable = findLocalVariable(name, asmType, checkType)
if (localVariable != null) {
return localVariable
}
getCapturedFieldNames(name).asSequence()
.mapNotNull { findCapturedLocalVariable(it, asmType, checkType) }
.firstOrNull()
?.let { return it }
}
}
return fail("Cannot find local variable: name = $name${if (checkType) ", type = " + asmType.toString() else ""}", failIfNotFound)
}
catch(e: InvalidStackFrameException) {
throw EvaluateExceptionUtil.createEvaluateException("Local variable $name is unavailable in current frame")
}
}
private fun fail(message: String, shouldFail: Boolean): Value? {
if (!shouldFail) {
return null
}
val location = frame?.location()
val locationText = location?.run { "Location: ${sourceName()}:${lineNumber()}" } ?: "No location available"
val sourceName = location?.sourceName()
val declaringTypeName = location?.declaringType()?.name()?.replace('.', '/')?.let { JvmClassName.byInternalName(it) }
val sourceFile = if (sourceName != null && declaringTypeName != null) {
DebuggerUtils.findSourceFileForClassIncludeLibrarySources(context.project, scope, declaringTypeName, sourceName, location)
} else {
null
}
val sourceFileText = runReadAction { sourceFile?.text }
if (sourceName != null && sourceFileText != null) {
val attachments = mergeAttachments(Attachment(sourceName, sourceFileText), Attachment("location.txt", locationText))
LOG.error(message, attachments)
}
throw EvaluateExceptionUtil.createEvaluateException(message)
}
private fun findThis(frame: StackFrame, asmType: Type?): Value? {
if (isInsideInlineFunctionBody(frame.visibleVariables())) {
val number = numberOfInlinedFunctions(frame.visibleVariables())
val inlineFunVar = findLocalVariableForInlineArgument("this_", number, asmType, true)
if (inlineFunVar != null) {
return inlineFunVar
}
}
// Captured labeled 'this'
frame.visibleVariables()
.asSequence()
.filter { it.name().startsWith(AsmUtil.LABELED_THIS_FIELD) }
.map { it to frame.getValue(it).asValue() }
.filter { isValueOfCorrectType(it.second, asmType, true) }
.maxBy { it.first }
?.let { return frame.getValue(it.first).asValue() }
val thisObject = frame.thisObject()
if (thisObject != null) {
val eval4jValue = thisObject.asValue()
if (isValueOfCorrectType(eval4jValue, asmType, true)) {
return eval4jValue
}
findThisInCapturedThis(thisObject, asmType)?.let { return it }
}
// TODO this is probably not needed anymore (covered by 'findThisInCapturedThis')
val receiver = findValue(RECEIVER_NAME, asmType, checkType = true, failIfNotFound = false)
if (receiver != null) return receiver
// TODO this is probably not needed anymore (covered by 'findThisInCapturedThis')
val this0 = findValue(AsmUtil.CAPTURED_THIS_FIELD, asmType, checkType = true, failIfNotFound = false)
if (this0 != null) return this0
// TODO this is probably not needed anymore (used only in JS)
val `$this` = findValue("\$this", asmType, checkType = false, failIfNotFound = false)
if (`$this` != null) return `$this`
return null
}
private fun findLocalVariableForLocalFun(name: String, asmType: Type?, checkType: Boolean): Value? {
return findLocalVariable(name + "$", asmType, checkType)
}
private fun findLocalVariableForInlineArgument(name: String, number: Int, asmType: Type?, checkType: Boolean): Value? {
return findLocalVariable(name + INLINE_FUN_VAR_SUFFIX.repeat(number), asmType, checkType)
}
private fun findThisInCapturedThis(capturedThis: ObjectReference, asmType: Type?): Value? {
for (field in capturedThis.referenceType().fields()) {
val name = field.name()
if (name == AsmUtil.CAPTURED_THIS_FIELD) {
val value = capturedThis.getValue(field)
if (value is ObjectReference) {
findThisInCapturedThis(value, asmType)?.let { return it }
}
}
if (name.startsWith(CAPTURED_FIELD_PREFIX + AsmUtil.LABELED_THIS_FIELD)
|| name == AsmUtil.CAPTURED_RECEIVER_FIELD
|| name == AsmUtil.CAPTURED_THIS_FIELD
) {
val value = capturedThis.getValue(field)
val evalValue = value.asValue()
if (isValueOfCorrectType(evalValue, asmType, true)) {
return evalValue
}
}
}
return null
}
private fun isFunctionType(type: Type?): Boolean {
return type?.sort == Type.OBJECT &&
type.internalName.startsWith(NUMBERED_FUNCTION_PREFIX)
}
private fun findLocalVariable(name: String, asmType: Type?, checkType: Boolean): Value? {
val localVariable = frame!!.visibleVariableByName(name) ?: return null
val eval4jValue = frame.getValue(localVariable).asValue()
val sharedVarValue = getValueIfSharedVar(eval4jValue, asmType, checkType)
if (sharedVarValue != null) {
return sharedVarValue
}
if (isValueOfCorrectType(eval4jValue, asmType, checkType)) {
return eval4jValue
}
return null
}
private fun findCapturedLocalVariable(name: String, asmType: Type?, checkType: Boolean): Value? {
val thisObject = frame?.thisObject() ?: return null
var thisObj: Value? = thisObject.asValue()
var capturedVal: Value? = null
while (capturedVal == null && thisObj != null) {
capturedVal = getField(thisObj, name, asmType, checkType)
if (capturedVal == null) {
thisObj = getField(thisObj, AsmUtil.CAPTURED_THIS_FIELD, null, false)
}
}
if (capturedVal != null) {
val sharedVarValue = getValueIfSharedVar(capturedVal, asmType, checkType)
if (sharedVarValue != null) {
return sharedVarValue
}
return capturedVal
}
return null
}
private fun isValueOfCorrectType(value: Value, asmType: Type?, shouldCheckType: Boolean): Boolean {
if (!shouldCheckType || asmType == null || value.asmType == asmType) return true
if (asmType == OBJECT_TYPE) return true
if ((value.obj() as? com.sun.jdi.ObjectReference)?.referenceType().isSubclass(asmType.className)) {
return true
}
val thisDesc = value.asmType.getClassDescriptor(scope)
val expDesc = asmType.getClassDescriptor(scope)
return thisDesc != null && expDesc != null && runReadAction { DescriptorUtils.isSubclass(thisDesc, expDesc) }
}
private fun getField(owner: Value, name: String, asmType: Type?, checkType: Boolean): Value? {
try {
val obj = owner.asJdiValue(frame!!.virtualMachine.virtualMachine, owner.asmType)
if (obj !is ObjectReference) return null
val _class = obj.referenceType()
val field = _class.fieldByName(name) ?: return null
val fieldValue = obj.getValue(field).asValue()
if (isValueOfCorrectType(fieldValue, asmType, checkType)) return fieldValue
return null
}
catch (e: Exception) {
return null
}
}
private fun Value.isSharedVar(): Boolean {
return this.asmType.sort == Type.OBJECT && this.asmType.internalName.startsWith(AsmTypes.REF_TYPE_PREFIX)
}
fun getValueIfSharedVar(value: Value, expectedType: Type?, checkType: Boolean): Value? {
if (!value.isSharedVar()) return null
val sharedVarValue = getField(value, "element", expectedType, checkType)
if (sharedVarValue != null && isValueOfCorrectType(sharedVarValue, expectedType, checkType)) {
return sharedVarValue
}
return null
}
private fun getCapturedFieldNames(name: String): List<String> = when (name) {
RECEIVER_NAME -> listOf(AsmUtil.CAPTURED_RECEIVER_FIELD)
THIS_NAME -> listOf(AsmUtil.CAPTURED_THIS_FIELD)
AsmUtil.CAPTURED_RECEIVER_FIELD -> listOf(name)
AsmUtil.CAPTURED_THIS_FIELD -> listOf(name)
else -> {
val simpleName = "$$name"
listOf(simpleName, simpleName + INLINE_TRANSFORMATION_SUFFIX)
}
}
private fun com.sun.jdi.Type?.isSubclass(superClassName: String): Boolean {
if (this !is ClassType) return false
if (allInterfaces().any { it.name() == superClassName }) {
return true
}
var superClass = this.superclass()
while (superClass != null) {
if (superClass.name() == superClassName) {
return true
}
superClass = superClass.superclass()
}
return false
}
}
@@ -32,6 +32,7 @@ import com.intellij.util.containers.MultiMap
import org.apache.log4j.Logger
import org.jetbrains.annotations.TestOnly
import org.jetbrains.eval4j.Value
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.analyzer.AnalysisResult
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
import org.jetbrains.kotlin.codegen.state.GenerationState
@@ -206,11 +207,12 @@ class KotlinDebuggerCaches(project: Project) {
}
private fun canBeEvaluatedInThisContext(compiledData: CompiledDataDescriptor, context: EvaluationContextImpl): Boolean {
val frameVisitor = FrameVisitor(context)
val variableFinder = VariableFinder.instance(context) ?: return false
return compiledData.parameters.all { p ->
val (name, jetType) = p
val value = frameVisitor.findValue(name, asmType = null, checkType = false, failIfNotFound = false)
if (value == null) return@all false
val lookupResult = variableFinder.find(name, null) ?: return@all false
val value = lookupResult.value.asValue()
val thisDescriptor = value.asmType.getClassDescriptor(context.debugProcess.searchScope)
val superClassDescriptor = jetType.constructor.declarationDescriptor as? ClassDescriptor
@@ -50,6 +50,8 @@ import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
import org.jetbrains.kotlin.caches.resolve.KotlinCacheService
import org.jetbrains.kotlin.codegen.*
import org.jetbrains.kotlin.codegen.AsmUtil.LABELED_THIS_PARAMETER
import org.jetbrains.kotlin.codegen.AsmUtil.RECEIVER_PARAMETER_NAME
import org.jetbrains.kotlin.codegen.binding.CodegenBinding
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.config.CompilerConfiguration
@@ -93,7 +95,6 @@ import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import java.util.*
internal val RECEIVER_NAME = "\$receiver"
internal val THIS_NAME = "this"
internal val LOG = Logger.getInstance("#org.jetbrains.kotlin.idea.debugger.evaluate.KotlinEvaluator")
internal val GENERATED_FUNCTION_NAME = "generated_for_debugger_fun"
@@ -411,7 +412,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
}
if (!AsmUtil.isPrimitive(parameterType) && AsmUtil.isPrimitive(argumentType)) {
if (parameterType == FrameVisitor.OBJECT_TYPE || parameterType == AsmUtil.boxType(argumentType)) {
if (parameterType.descriptor == "Ljava/lang/Object;" || parameterType == AsmUtil.boxType(argumentType)) {
return eval.boxType(argumentValue)
}
}
@@ -437,8 +438,14 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
}
val vm = context.debugProcess.virtualMachineProxy.virtualMachine
val sharedVar = FrameVisitor(context).getValueIfSharedVar(jdiValue, jdiValue.asmType, false)
return sharedVar?.asJdiValue(vm, sharedVar.asmType) ?: jdiValue.asJdiValue(vm, jdiValue.asmType)
val sharedVar = getValueIfSharedVar(jdiValue)
return sharedVar?.value ?: jdiValue.asJdiValue(vm, jdiValue.asmType)
}
private fun getValueIfSharedVar(value: Value): VariableFinder.Result? {
val obj = value.obj(value.asmType) as? ObjectReference ?: return null
return VariableFinder.Result(VariableFinder.unwrapRefValue(obj))
}
private fun ExtractionResult.getParametersForDebugger(
@@ -505,12 +512,11 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
parameters: List<Parameter>,
parameterTypes: Array<Type>
): List<Value> {
val frameVisitor = FrameVisitor(this)
val variableFinder = VariableFinder.instance(this) ?: error("No stack frame available")
return parameters.zip(parameterTypes).map { (parameter, type) ->
parameter.error?.let { throw it }
val result = parameter.value
?: frameVisitor.findValue(parameter.callText, type, checkType = false, failIfNotFound = true)!!
val result = parameter.value ?: variableFinder.get(parameter.callText, type).asValue()
if (LOG.isDebugEnabled) {
LOG.debug("Parameter for eval4j: name = ${parameter.callText}, type = $type, value = $result")
@@ -549,10 +555,14 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
CompilerConfiguration.EMPTY
).generateDeclaredClassFilter(generateClassFilter).build()
val frameVisitor = FrameVisitor(context)
val variableFinder = VariableFinder.instance(context) ?: error("No stack frame available")
val extractedFunctionName = extractedFunction.name
?: error("Extracted function has an empty name: ${extractedFunction.text}")
extractedFunction.receiverTypeReference?.let {
state.bindingTrace.recordAnonymousType(it, THIS_NAME, frameVisitor)
val name = AsmUtil.getLabeledThisName(extractedFunctionName, LABELED_THIS_PARAMETER, RECEIVER_PARAMETER_NAME)
state.bindingTrace.recordAnonymousType(it, name, variableFinder)
}
val valueParameters = extractedFunction.valueParameters
@@ -569,7 +579,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
exception("An exception occurs during Evaluate Expression Action")
}
state.bindingTrace.recordAnonymousType(paramRef, param.callText, frameVisitor)
state.bindingTrace.recordAnonymousType(paramRef, param.callText, variableFinder)
}
KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION)
@@ -578,13 +588,19 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
}
}
private fun BindingTrace.recordAnonymousType(typeReference: KtTypeReference, localVariableName: String, visitor: FrameVisitor) {
private fun BindingTrace.recordAnonymousType(
typeReference: KtTypeReference,
localVariableName: String,
variableFinder: VariableFinder
) {
val paramAnonymousType = typeReference.debugTypeInfo
if (paramAnonymousType != null) {
val declarationDescriptor = paramAnonymousType.constructor.declarationDescriptor
if (declarationDescriptor is ClassDescriptor) {
val localVariable = visitor.findValue(localVariableName, asmType = null, checkType = false, failIfNotFound = false)
?: exception("Couldn't find local variable this in current frame to get classType for anonymous type $paramAnonymousType}")
val lookupResult = variableFinder.find(localVariableName, null)
?: exception("Couldn't find local variable this in current frame to get classType for anonymous type $paramAnonymousType}")
val localVariable = lookupResult.value.asValue()
record(CodegenBinding.ASM_TYPE, declarationDescriptor, localVariable.asmType)
if (LOG.isDebugEnabled) {
LOG.debug("Asm type ${localVariable.asmType.className} was recorded for ${declarationDescriptor.name}")
@@ -0,0 +1,342 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.openapi.diagnostic.Attachment
import com.sun.jdi.*
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.AsmUtil.getCapturedFieldName
import org.jetbrains.kotlin.codegen.AsmUtil.getLabeledThisName
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.codegen.inline.INLINE_TRANSFORMATION_SUFFIX
import org.jetbrains.kotlin.codegen.inline.NUMBERED_FUNCTION_PREFIX
import org.jetbrains.kotlin.codegen.topLevelClassAsmType
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.attachment.mergeAttachments
import org.jetbrains.kotlin.load.java.JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT
import org.jetbrains.kotlin.load.java.JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType
import org.jetbrains.org.objectweb.asm.Type as AsmType
import com.sun.jdi.Type as JdiType
class VariableFinder private constructor(private val context: EvaluationContextImpl, private val frameProxy: StackFrameProxyImpl) {
companion object {
fun instance(context: EvaluationContextImpl): VariableFinder? {
val frameProxy = context.frameProxy ?: return null
return VariableFinder(context, frameProxy)
}
fun unwrapRefValue(value: ObjectReference): Value? {
return NamedEntity("<nameForUnwrapOnly>", value.type()) { value }.unwrap().value()
}
private val inlinedThisRegex = getLocalVariableNameRegexInlineAware(AsmUtil.THIS + "_")
private fun getCapturedVariableNameRegex(capturedName: String): Regex {
val escapedName = Regex.escape(capturedName)
val escapedSuffix = Regex.escape(INLINE_TRANSFORMATION_SUFFIX)
return Regex("^$escapedName(?:$escapedSuffix)?$")
}
private fun getLocalVariableNameRegexInlineAware(name: String): Regex {
val escapedName = Regex.escape(name)
val escapedSuffix = Regex.escape(INLINE_FUN_VAR_SUFFIX)
return Regex("^$escapedName(?:$escapedSuffix)*$")
}
private fun AsmType.isFunctionType(): Boolean {
return sort == AsmType.OBJECT && internalName.startsWith(NUMBERED_FUNCTION_PREFIX)
}
fun getInlineDepth(variables: List<LocalVariableProxyImpl>): Int {
val inlineFunVariables = variables.filter { it.name().startsWith(LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION) }
if (inlineFunVariables.isEmpty()) {
return 0
}
val closestInlineFun = inlineFunVariables.maxBy { it.variable }!!.variable
val inlineLambdaDepth = variables
.count { it.name().startsWith(LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT) && it.variable > closestInlineFun }
return maxOf(0, inlineFunVariables.size - inlineLambdaDepth)
}
fun getInlineDepth(variableName: String): Int {
var endIndex = variableName.length
var depth = 0
val suffixLen = INLINE_FUN_VAR_SUFFIX.length
while (endIndex >= suffixLen) {
if (variableName.substring(endIndex - suffixLen, endIndex) != INLINE_FUN_VAR_SUFFIX) {
break
}
depth++
endIndex -= suffixLen
}
return depth
}
}
sealed class VariableKind(val type: AsmType?) {
abstract fun capturedNameMatches(name: String): Boolean
class Ordinary(val name: String, type: AsmType?) : VariableKind(type) {
private val capturedNameRegex = getCapturedVariableNameRegex(getCapturedFieldName(this.name))
override fun capturedNameMatches(name: String) = capturedNameRegex.matches(name)
}
class UnlabeledThis(type: AsmType?) : VariableKind(type) {
override fun capturedNameMatches(name: String) =
(name == AsmUtil.CAPTURED_RECEIVER_FIELD || name.startsWith(AsmUtil.getCapturedFieldName(AsmUtil.LABELED_THIS_FIELD)))
}
class LabeledThis(val label: String, type: AsmType?) : VariableKind(type) {
private val capturedNameRegex = getCapturedVariableNameRegex(
getCapturedFieldName(getLabeledThisName(label, AsmUtil.LABELED_THIS_FIELD, AsmUtil.CAPTURED_RECEIVER_FIELD))
)
override fun capturedNameMatches(name: String) = capturedNameRegex.matches(name)
}
fun typeMatches(type: JdiType?): Boolean {
if (type == null) return true
val asmType = this.type ?: return true
// Main path
if (asmType.descriptor == "Ljava/lang/Object;" || type.isSubtype(asmType)) {
return true
}
// The latter is for boxing interventions
fun box(desc: String) = JvmPrimitiveType.getByDesc(desc)?.wrapperFqName?.topLevelClassAsmType()?.descriptor
val asmTypeDescriptor = asmType.descriptor
val jdiTypeDescriptor = type.signature()
val boxedAsmType = box(asmTypeDescriptor) ?: asmTypeDescriptor
val boxedJdiType = box(jdiTypeDescriptor) ?: jdiTypeDescriptor
return boxedAsmType == boxedJdiType
}
}
class Result(val value: Value?)
private class NamedEntity(val name: String, val type: JdiType?, val value: () -> Value?) {
companion object {
fun of(field: Field, owner: ObjectReference): NamedEntity {
return NamedEntity(field.name(), field.safeType()) { owner.getValue(field) }.unwrap()
}
fun of(variable: LocalVariableProxyImpl, frameProxy: StackFrameProxyImpl): NamedEntity {
return NamedEntity(variable.name(), variable.safeType()) { frameProxy.getValue(variable) }.unwrap()
}
}
fun unwrap(): NamedEntity {
if (type !is ClassType || !type.signature().startsWith("L" + AsmTypes.REF_TYPE_PREFIX)) {
return this
}
val obj = this.value() as? ObjectReference ?: return this
val field = type.fieldByName("element") ?: return this
val unwrappedValue = obj.getValue(field)
val unwrappedType = if (field.type() is PrimitiveType) field.type() else unwrappedValue?.type()
return NamedEntity(name, unwrappedType) { unwrappedValue }
}
}
fun get(name: String, type: AsmType?): Value? {
val result = find(name, type) ?: throw variableNotFound(buildString {
append("Cannot find local variable: name = '").append(name).append("'")
if (type != null) {
append(", type = " + type.className)
}
})
return result.value
}
fun find(name: String, type: AsmType?): Result? {
return when {
name.startsWith(AsmUtil.THIS + "@") -> {
val label = name.drop(AsmUtil.THIS.length + 1).also { require(it.isNotEmpty()) { "Invalid name '$name'" } }
findLabeledThis(VariableKind.LabeledThis(label, type))
}
name == AsmUtil.THIS -> findUnlabeledThis(VariableKind.UnlabeledThis(type))
else -> findOrdinary(VariableKind.Ordinary(name, type))
}
}
private fun findOrdinary(kind: VariableKind.Ordinary): Result? {
val variables = frameProxy.safeVisibleVariables()
// Local variables direct search
findLocalVariable(variables, kind, kind.name)?.let { return it }
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
// Recursive search in captured this
val containingThis = frameProxy.thisObject() ?: return null
return findCapturedVariable(kind, containingThis)
}
private fun findLabeledThis(kind: VariableKind.LabeledThis): Result? {
val variables = frameProxy.safeVisibleVariables()
// Local variables direct search
findLocalVariable(variables, kind, AsmUtil.LABELED_THIS_PARAMETER + kind.label)?.let { return it }
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
// Recursive search in captured this
val containingThis = frameProxy.thisObject()
if (containingThis != null) {
findCapturedVariable(kind, containingThis)?.let { return it }
}
// Fallback: find an unlabeled this with the compatible type
return findUnlabeledThis(VariableKind.UnlabeledThis(kind.type))
}
private fun findUnlabeledThis(kind: VariableKind.UnlabeledThis): Result? {
val variables = frameProxy.safeVisibleVariables()
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
val containingThis = frameProxy.thisObject() ?: return null
return findCapturedVariable(kind, containingThis)
}
private fun findLocalVariable(variables: List<LocalVariableProxyImpl>, kind: VariableKind, name: String): Result? {
variables.namedEntitySequence()
.filter { it.name == name && kind.typeMatches(it.type) }
.firstOrNull()
?.let { return Result(it.value()) }
val canBeLocalFunction = kind is VariableKind.Ordinary
if (canBeLocalFunction && kind.type?.isFunctionType() == true) {
@Suppress("ConvertToStringTemplate")
variables.namedEntitySequence()
.filter { it.name == name + "$" && kind.typeMatches(it.type) }
.firstOrNull()
?.let { return Result(it.value()) }
}
val nameInlineAwareRegex = getLocalVariableNameRegexInlineAware(name)
val inlineDepth = getInlineDepth(variables)
variables.namedEntitySequence()
.filter { it.name.matches(nameInlineAwareRegex) && getInlineDepth(it.name) == inlineDepth && kind.typeMatches(it.type) }
.toList() // Sorted by will make a list anyway
.firstOrNull()
?.let { return Result(it.value()) }
return null
}
private fun findCapturedVariableInReceiver(variables: List<LocalVariableProxyImpl>, kind: VariableKind): Result? {
fun isReceiverOrPassedThis(name: String) =
name.startsWith(AsmUtil.LABELED_THIS_PARAMETER)
|| name == AsmUtil.RECEIVER_PARAMETER_NAME
|| name == AsmUtil.getCapturedFieldName(AsmUtil.THIS)
|| inlinedThisRegex.matches(name) // org.jetbrains.kotlin.codegen.inline.MethodInliner.prepareNode
if (kind is VariableKind.LabeledThis) {
variables.namedEntitySequence()
.filter { kind.capturedNameMatches(it.name) && kind.typeMatches(it.type) }
.firstOrNull()
?.let { return Result(it.value()) }
}
return variables.namedEntitySequence()
.filter { isReceiverOrPassedThis(it.name) && it.type is ReferenceType? }
.mapNotNull { findCapturedVariable(kind, it.value()) }
.firstOrNull()
}
private fun findCapturedVariable(kind: VariableKind, parent: Value?): Result? {
if (parent !is ObjectReference) return null
if (kind is VariableKind.UnlabeledThis && kind.typeMatches(parent.type())) {
return Result(parent)
}
val fields = parent.referenceType().fields()
// Captured variables - direct search
fields.namedEntitySequence(parent)
.filter { kind.capturedNameMatches(it.name) && kind.typeMatches(it.type) }
.firstOrNull()
?.let { return Result(it.value()) }
// Recursive search in captured receivers
fields.namedEntitySequence(parent)
.filter { isCapturedReceiverFieldName(it.name) && it.type is ReferenceType? }
.mapNotNull { findCapturedVariable(kind, it.value()) }
.firstOrNull()
?.let { return it }
// Recursive search in outer and captured this
fields.namedEntitySequence(parent)
.filter { it.name == AsmUtil.getCapturedFieldName(AsmUtil.THIS) || it.name == AsmUtil.CAPTURED_THIS_FIELD }
.filter { it.type is ReferenceType? }
.firstOrNull()
?.let { return findCapturedVariable(kind, it.value()) }
return null
}
private fun isCapturedReceiverFieldName(name: String): Boolean {
return name.startsWith(getCapturedFieldName(AsmUtil.LABELED_THIS_FIELD))
|| name == AsmUtil.CAPTURED_RECEIVER_FIELD
}
private fun variableNotFound(message: String): Exception {
val location = frameProxy.location()
val scope = context.debugProcess.searchScope
val locationText = location?.run { "Location: ${sourceName()}:${lineNumber()}" } ?: "No location available"
val sourceName = location?.sourceName()
val declaringTypeName = location?.declaringType()?.name()?.replace('.', '/')?.let { JvmClassName.byInternalName(it) }
val sourceFile = if (sourceName != null && declaringTypeName != null) {
DebuggerUtils.findSourceFileForClassIncludeLibrarySources(context.project, scope, declaringTypeName, sourceName, location)
} else {
null
}
val sourceFileText = runReadAction { sourceFile?.text }
if (sourceName != null && sourceFileText != null) {
val attachments = mergeAttachments(
Attachment(sourceName, sourceFileText),
Attachment("location.txt", locationText)
)
LOG.error(message, attachments)
}
return EvaluateExceptionUtil.createEvaluateException(message)
}
private fun List<Field>.namedEntitySequence(owner: ObjectReference) = asSequence().map { NamedEntity.of(it, owner) }
private fun List<LocalVariableProxyImpl>.namedEntitySequence() = asSequence().map { NamedEntity.of(it, frameProxy) }
}
@@ -0,0 +1,93 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.engine.evaluation.AbsentInformationEvaluateException
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.sun.jdi.*
fun StackFrameProxyImpl.safeVisibleVariables(): List<LocalVariableProxyImpl> {
return try {
visibleVariables()
} catch (e: AbsentInformationEvaluateException) {
// Current implementation of visibleVariables() wraps an AbsentInformationException into EvaluateException
emptyList()
} catch (e: AbsentInformationException) {
emptyList()
}
}
fun Method.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun ReferenceType.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun Method.safeLocationsOfLine(line: Int): List<Location> {
return try {
locationsOfLine(line)
} catch (e: AbsentInformationException) {
emptyList()
}
}
fun Method.safeVariables(): List<LocalVariable>? {
return try {
variables()
} catch (e: AbsentInformationException) {
null
}
}
fun Method.safeArguments(): List<LocalVariable>? {
return try {
arguments()
} catch (e: AbsentInformationException) {
null
}
}
fun Location.safeSourceName(): String? {
return try {
sourceName()
} catch (e: AbsentInformationException) {
null
} catch (e: InternalError) {
null
}
}
fun Location.safeLineNumber(): Int {
return DebuggerUtilsEx.getLineNumber(this, false)
}
fun Location.safeSourceLineNumber(): Int {
return DebuggerUtilsEx.getLineNumber(this, true)
}
fun Location.safeMethod(): Method? {
return DebuggerUtilsEx.getMethod(this)
}
fun LocalVariableProxyImpl.safeType(): Type? {
return try {
type
} catch (e: ClassNotLoadedException) {
null
}
}
fun Field.safeType(): Type? {
return try {
type()
} catch (e: ClassNotLoadedException) {
null
}
}
@@ -523,7 +523,7 @@ private fun SuspendContextImpl.getNextPositionWithFilter(
}
fun getInlineRangeLocalVariables(stackFrame: StackFrameProxyImpl): List<LocalVariable> {
return stackFrame.visibleVariablesSafe()
return stackFrame.safeVisibleVariables()
.filter {
val name = it.name()
name.startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION)
@@ -41,11 +41,9 @@ Compile bytecode for extClass.testCompPrivate()
extensionMemberFunction.kt:88
Compile bytecode for testCompPrivate()
extensionMemberFunction.kt:98
Compile bytecode for testCompPublic()
extensionMemberFunction.kt:103
Compile bytecode for this.testCompPublic()
extensionMemberFunction.kt:108
Compile bytecode for testCompPrivate()
extensionMemberFunction.kt:113
Compile bytecode for this.testCompPrivate()
Disconnected from the target VM
@@ -41,11 +41,9 @@ Compile bytecode for extClass.testCompPrivate
extensionMemberProperty.kt:92
Compile bytecode for testCompPrivate
extensionMemberProperty.kt:102
Compile bytecode for testCompPublic
extensionMemberProperty.kt:107
Compile bytecode for this.testCompPublic
extensionMemberProperty.kt:112
Compile bytecode for testCompPrivate
extensionMemberProperty.kt:117
Compile bytecode for this.testCompPrivate
Disconnected from the target VM
@@ -23,7 +23,7 @@ class Outer {
// outer isn't captured in lambda
lambda {
// EXPRESSION: foo() + 2
// RESULT: java.lang.AssertionError : Cannot find local variable: name = this
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'this@Outer', type = funFromOuterClassInLamdba.Outer
//Breakpoint!
val a = 1
}
@@ -16,7 +16,7 @@ fun main(args: Array<String>) {
fun myLocalFun3() {
// EXPRESSION: myLocalFun1() + 1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = myLocalFun1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'myLocalFun1', type = kotlin.jvm.functions.Function0
//Breakpoint!
myLocalFun1() + 1
}
@@ -56,7 +56,7 @@ fun main(args: Array<String>) {
i = 1
fun myLocalFun7() {
// EXPRESSION: myLocalFun6() + 1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = myLocalFun6
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'myLocalFun6', type = kotlin.jvm.functions.Function0
//Breakpoint!
myLocalFun6() + 1
}
@@ -15,4 +15,4 @@ fun foo(f: () -> Unit) {
// PRINT_FRAME
// EXPRESSION: val1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = val1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'val1', type = int
@@ -20,7 +20,7 @@ fun foo(f: () -> Unit) {
// PRINT_FRAME
// EXPRESSION: val1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = val1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'val1', type = int
frame = invoke:7, FrameLambdaNotUsedKt$main$1 {frameLambdaNotUsed}
this = this = {frameLambdaNotUsed.FrameLambdaNotUsedKt$main$1@uniqueID}Function0<kotlin.Unit>
field = arity: int = 0 (sp = Lambda.!EXT!)