Debugger: Fix lvalue evaluation (KT-11663, KT-19980)

This commit is contained in:
Yan Zhulanow
2019-04-18 23:41:37 +03:00
parent bb0bc8a38a
commit 0a6a811c57
12 changed files with 174 additions and 51 deletions
@@ -36,6 +36,7 @@ class CodeFragmentCodegenInfo(
interface IParameter {
val targetDescriptor: DeclarationDescriptor
val targetType: KotlinType
val isLValue: Boolean
}
}
@@ -210,7 +211,7 @@ class CodeFragmentCodegen private constructor(
val asmType: Type
val stackValue: StackValue
val sharedAsmType = getSharedTypeIfApplicable(parameter.targetDescriptor, typeMapper)
val sharedAsmType = getSharedTypeIfApplicable(parameter, typeMapper)
if (sharedAsmType != null) {
asmType = sharedAsmType
val unwrappedType = typeMapper.mapType(parameter.targetType)
@@ -229,9 +230,15 @@ class CodeFragmentCodegen private constructor(
return CalculatedCodeFragmentCodegenInfo(parameters, methodSignature.returnType)
}
fun getSharedTypeIfApplicable(descriptor: DeclarationDescriptor, typeMapper: KotlinTypeMapper): Type? {
return when (descriptor) {
is LocalVariableDescriptor -> typeMapper.getSharedVarType(descriptor)
fun getSharedTypeIfApplicable(parameter: IParameter, typeMapper: KotlinTypeMapper): Type? {
return when (val descriptor = parameter.targetDescriptor) {
is LocalVariableDescriptor -> {
var result = typeMapper.getSharedVarType(descriptor)
if (result == null && parameter.isLValue) {
result = StackValue.sharedTypeForType(typeMapper.mapType(descriptor.type))
}
result
}
else -> null
}
}
@@ -110,15 +110,21 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
return qualifiedExpression.getOperationSign() == KtTokens.DOT &&
qualifiedExpression.getReceiverExpression() == KtPsiUtil.deparenthesize(expression);
}
if (parent instanceof KtBinaryExpression) {
KtBinaryExpression binaryExpression = (KtBinaryExpression) parent;
if (!OperatorConventions.BINARY_OPERATION_NAMES.containsKey(binaryExpression.getOperationToken()) &&
!KtTokens.ALL_ASSIGNMENTS.contains(binaryExpression.getOperationToken())) {
return false;
}
return PsiTreeUtil.isAncestor(binaryExpression.getLeft(), expression, false);
return isLValue(expression, parent);
}
public static boolean isLValue(@NotNull KtSimpleNameExpression expression, @Nullable PsiElement parent) {
if (!(parent instanceof KtBinaryExpression)) {
return false;
}
return false;
KtBinaryExpression binaryExpression = (KtBinaryExpression) parent;
if (!OperatorConventions.BINARY_OPERATION_NAMES.containsKey(binaryExpression.getOperationToken()) &&
!KtTokens.ALL_ASSIGNMENTS.contains(binaryExpression.getOperationToken())) {
return false;
}
return PsiTreeUtil.isAncestor(binaryExpression.getLeft(), expression, false);
}
private static boolean isDangerousWithNull(@NotNull KtSimpleNameExpression expression, @NotNull ExpressionTypingContext context) {
@@ -1463,7 +1469,9 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
@NotNull
private KotlinTypeInfo assignmentIsNotAnExpressionError(KtBinaryExpression expression, ExpressionTypingContext context) {
facade.checkStatementType(expression, context);
context.trace.report(ASSIGNMENT_IN_EXPRESSION_CONTEXT.on(expression));
if (!context.isDebuggerContext) {
context.trace.report(ASSIGNMENT_IN_EXPRESSION_CONTEXT.on(expression));
}
return TypeInfoFactoryKt.noTypeInfo(context);
}
@@ -373,7 +373,9 @@ public class DataFlowAnalyzer {
public KotlinTypeInfo illegalStatementType(@NotNull KtExpression expression, @NotNull ExpressionTypingContext context, @NotNull ExpressionTypingInternals facade) {
facade.checkStatementType(
expression, context.replaceExpectedType(TypeUtils.NO_EXPECTED_TYPE).replaceContextDependency(INDEPENDENT));
context.trace.report(EXPRESSION_EXPECTED.on(expression, expression));
if (!context.isDebuggerContext) {
context.trace.report(EXPRESSION_EXPECTED.on(expression, expression));
}
return TypeInfoFactoryKt.noTypeInfo(context);
}
@@ -55,6 +55,7 @@ import org.jetbrains.kotlin.idea.debugger.evaluate.variables.EvaluatorValueConve
import org.jetbrains.kotlin.idea.debugger.evaluate.variables.VariableFinder
import org.jetbrains.kotlin.idea.debugger.safeLocation
import org.jetbrains.kotlin.idea.debugger.safeMethod
import org.jetbrains.kotlin.idea.debugger.safeVisibleVariableByName
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
@@ -287,18 +288,38 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
for (parameterType in compiledData.mainMethodSignature.parameterTypes) {
context.findClass(parameterType, classLoader)
}
val args = calculateMainMethodCallArguments(context, compiledData)
block(args)
val variableFinder = VariableFinder(context)
val args = calculateMainMethodCallArguments(variableFinder, compiledData)
val result = block(args)
for (wrapper in variableFinder.refWrappers) {
updateLocalVariableValue(variableFinder.evaluatorValueConverter, wrapper)
}
return@executeWithBreakpointsDisabled result
}
}
private fun calculateMainMethodCallArguments(context: ExecutionContext, compiledData: CompiledDataDescriptor): List<Value?> {
private fun updateLocalVariableValue(converter: EvaluatorValueConverter, ref: VariableFinder.RefWrapper) {
val frameProxy = converter.context.frameProxy
val variable = frameProxy.safeVisibleVariableByName(ref.localVariableName) ?: return
val newValue = converter.unref(ref.wrapper)
try {
frameProxy.setValue(variable, newValue)
} catch (e: InvalidTypeException) {
LOG.error("Cannot update local variable value: expected type ${variable.type}, actual type ${newValue?.type()}", e)
}
}
private fun calculateMainMethodCallArguments(variableFinder: VariableFinder, compiledData: CompiledDataDescriptor): List<Value?> {
val asmValueParameters = compiledData.mainMethodSignature.parameterTypes
val valueParameters = compiledData.parameters
require(asmValueParameters.size == valueParameters.size)
val args = valueParameters.zip(asmValueParameters)
val variableFinder = VariableFinder(context)
return args.map { (parameter, asmType) ->
val result = variableFinder.find(parameter, asmType)
@@ -307,7 +328,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
val name = parameter.debugString
fun isInsideDefaultInterfaceMethod(): Boolean {
val method = context.frameProxy.safeLocation()?.safeMethod() ?: return false
val method = variableFinder.context.frameProxy.safeLocation()?.safeMethod() ?: return false
val desc = method.signature()
return method.name().endsWith("\$default") && DEFAULT_METHOD_MARKERS.any { desc.contains("I${it.descriptor})") }
}
@@ -319,7 +340,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
} else if (parameter.kind == CodeFragmentParameter.Kind.ORDINARY && isInsideDefaultInterfaceMethod()) {
evaluationException("Parameter evaluation is not supported for '\$default' methods")
} else {
throw VariableFinder.variableNotFound(context, buildString {
throw VariableFinder.variableNotFound(variableFinder.context, buildString {
append("Cannot find local variable: name = '").append(name).append("', type = ").append(asmType.className)
})
}
@@ -94,7 +94,7 @@ class CodeFragmentCompiler(private val executionContext: ExecutionContext) {
val classes = generationState.factory.asList().filterClassFiles()
.map { ClassToLoad(it.internalClassName, it.relativePath, it.asByteArray()) }
val methodSignature = getMethodSignature(methodDescriptor, parameterInfo.parameters, generationState)
val methodSignature = getMethodSignature(methodDescriptor, parameterInfo, generationState)
val functionSuffixes = getLocalFunctionSuffixes(parameterInfo.parameters, generationState.typeMapper)
generationState.destroy()
@@ -123,13 +123,14 @@ class CodeFragmentCompiler(private val executionContext: ExecutionContext) {
private fun getMethodSignature(
methodDescriptor: FunctionDescriptor,
parameters: List<CodeFragmentParameter.Smart>,
parameterInfo: CodeFragmentParameterInfo,
state: GenerationState
): MethodSignature {
val typeMapper = state.typeMapper
val asmSignature = typeMapper.mapSignatureSkipGeneric(methodDescriptor)
val asmParameters = parameters.zip(asmSignature.valueParameters).map { (param, sigParam) ->
getSharedTypeIfApplicable(param.targetDescriptor, typeMapper) ?: sigParam.asmType
val asmParameters = parameterInfo.parameters.zip(asmSignature.valueParameters).map { (param, sigParam) ->
getSharedTypeIfApplicable(param, typeMapper) ?: sigParam.asmType
}
return MethodSignature(asmParameters, asmSignature.returnType)
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.debugger.evaluate.compilation
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.CodeFragmentCodegenInfo
import org.jetbrains.kotlin.codegen.getCallLabelForLambdaArgument
@@ -34,6 +35,7 @@ import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.expressions.BasicExpressionTypingVisitor
import org.jetbrains.kotlin.types.expressions.createFunctionType
class CodeFragmentParameterInfo(
@@ -140,7 +142,8 @@ class CodeFragmentParameterAnalyzer(
private fun processDescriptor(descriptor: DeclarationDescriptor, expression: KtSimpleNameExpression) {
val parameter = processDebugLabel(descriptor)
?: processCoroutineContextCall(descriptor)
?: processSimpleNameExpression(descriptor)
?: processSimpleNameExpression(descriptor, expression)
checkBounds(descriptor, expression, parameter)
}
@@ -255,7 +258,7 @@ class CodeFragmentParameterAnalyzer(
}
}
private fun processSimpleNameExpression(target: DeclarationDescriptor): Smart? {
private fun processSimpleNameExpression(target: DeclarationDescriptor, expression: KtSimpleNameExpression): Smart? {
if (target is ValueParameterDescriptor && target.isCrossinline) {
throw EvaluateExceptionUtil.createEvaluateException("Evaluation of 'crossinline' lambdas is not supported")
}
@@ -278,11 +281,14 @@ class CodeFragmentParameterAnalyzer(
}
}
is ValueDescriptor -> {
val parent = PsiTreeUtil.skipParentsOfType(expression, KtParenthesizedExpression::class.java)
val isLValue = BasicExpressionTypingVisitor.isLValue(expression, parent)
parameters.getOrPut(target) {
val type = target.type
@Suppress("DEPRECATION")
val kind = if (target is LocalVariableDescriptor && target.isDelegated) Kind.DELEGATED else Kind.ORDINARY
Smart(Dumb(kind, target.name.asString()), type, target)
Smart(Dumb(kind, target.name.asString()), type, target, isLValue)
}
}
else -> null
@@ -17,7 +17,7 @@ import com.sun.jdi.Type as JdiType
import kotlin.jvm.internal.Ref
@Suppress("SpellCheckingInspection")
class EvaluatorValueConverter(private val context: ExecutionContext) {
class EvaluatorValueConverter(val context: ExecutionContext) {
private companion object {
private val UNBOXING_METHOD_NAMES = mapOf(
"java/lang/Boolean" to "booleanValue",
@@ -228,9 +228,12 @@ private fun unwrap(type: AsmType): AsmType {
private val AsmType.isPrimitiveType: Boolean
get() = sort != AsmType.OBJECT && sort != AsmType.ARRAY
private val AsmType.isRefType: Boolean
internal val AsmType.isRefType: Boolean
get() = sort == AsmType.OBJECT && this in REF_TYPES
internal val Value?.isRefType: Boolean
get() = this is ObjectReference && AsmType.getType(this.referenceType().signature()).isRefType
private val AsmType.isBoxedType: Boolean
get() = this in BOXED_TO_PRIMITIVE
@@ -30,11 +30,12 @@ import kotlin.coroutines.Continuation
import org.jetbrains.org.objectweb.asm.Type as AsmType
import com.sun.jdi.Type as JdiType
class VariableFinder(private val context: ExecutionContext) {
class VariableFinder(val context: ExecutionContext) {
private val frameProxy = context.frameProxy
companion object {
private const val USE_UNSAFE_FALLBACK = true
private val USE_UNSAFE_FALLBACK: Boolean
get() = true
fun variableNotFound(context: ExecutionContext, message: String): Exception {
val frameProxy = context.frameProxy
@@ -73,7 +74,14 @@ class VariableFinder(private val context: ExecutionContext) {
}
}
private val evaluatorValueConverter = EvaluatorValueConverter(context)
val evaluatorValueConverter = EvaluatorValueConverter(context)
val refWrappers: List<RefWrapper>
get() = mutableRefWrappers
private val mutableRefWrappers = mutableListOf<RefWrapper>()
class RefWrapper(val localVariableName: String, val wrapper: Value?)
sealed class VariableKind(val asmType: AsmType) {
abstract fun capturedNameMatches(name: String): Boolean
@@ -273,8 +281,26 @@ class VariableFinder(private val context: ExecutionContext) {
): Result? {
val inlineDepth = getInlineDepth(variables)
findLocalVariable(variables, kind, inlineDepth, namePredicate)?.let { return it }
// Try to find variables outside of inline functions as well
if (inlineDepth > 0 && USE_UNSAFE_FALLBACK) {
findLocalVariable(variables, kind, 0, namePredicate)?.let { return it }
}
return null
}
private fun findLocalVariable(
variables: List<LocalVariableProxyImpl>,
kind: VariableKind,
inlineDepth: Int,
namePredicate: (String) -> Boolean
): Result? {
val actualPredicate: (String) -> Boolean
if (inlineDepth > 0) {
val inlineAwareNamePredicate = fun(name: String): Boolean {
actualPredicate = fun(name: String): Boolean {
var endIndex = name.length
var depth = 0
@@ -288,21 +314,27 @@ class VariableFinder(private val context: ExecutionContext) {
endIndex -= suffixLen
}
return namePredicate(name.take(endIndex))
return namePredicate(name.take(endIndex)) && getInlineDepth(name) == inlineDepth
}
variables.namedEntitySequence()
.filter { inlineAwareNamePredicate(it.name) && getInlineDepth(it.name) == inlineDepth && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
} else {
actualPredicate = namePredicate
}
variables.namedEntitySequence()
.filter { namePredicate(it.name) && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
for (item in variables.namedEntitySequence()) {
if (!actualPredicate(item.name) || !kind.typeMatches(item.type)) {
continue
}
val rawValue = item.value()
val result = evaluatorValueConverter.coerce(getUnwrapDelegate(kind, rawValue), kind.asmType) ?: continue
if (!rawValue.isRefType && result.value.isRefType) {
// Local variable was wrapped into a Ref instance
mutableRefWrappers += RefWrapper(item.name, result.value)
}
return result
}
return null
}
@@ -377,7 +409,7 @@ class VariableFinder(private val context: ExecutionContext) {
}
private fun findCapturedVariable(kind: VariableKind, parentFactory: () -> Value?): Result? {
val parent = getUnwrapDelegate(kind, parentFactory)
val parent = getUnwrapDelegate(kind, parentFactory())
return findCapturedVariable(kind, parent)
}
@@ -415,8 +447,7 @@ class VariableFinder(private val context: ExecutionContext) {
return null
}
private fun getUnwrapDelegate(kind: VariableKind, valueFactory: () -> Value?): Value? {
val rawValue = valueFactory()
private fun getUnwrapDelegate(kind: VariableKind, rawValue: Value?): Value? {
if (kind !is VariableKind.Ordinary || !kind.isDelegated) {
return rawValue
}
@@ -443,8 +474,8 @@ class VariableFinder(private val context: ExecutionContext) {
return evaluatorValueConverter.typeMatches(asmType, actualType)
}
private fun NamedEntity.unwrapAndCheck(kind: VariableKind): Result? {
return evaluatorValueConverter.coerce(getUnwrapDelegate(kind, value), kind.asmType)
private fun NamedEntity.unwrapAndCheck(kind: VariableKind, value: () -> Value? = this.value): Result? {
return evaluatorValueConverter.coerce(getUnwrapDelegate(kind, value()), kind.asmType)
}
private fun List<Field>.namedEntitySequence(owner: ObjectReference): Sequence<NamedEntity> {
@@ -22,7 +22,8 @@ interface CodeFragmentParameter {
class Smart(
val dumb: Dumb,
override val targetType: KotlinType,
override val targetDescriptor: DeclarationDescriptor
override val targetDescriptor: DeclarationDescriptor,
override val isLValue: Boolean = false
) : CodeFragmentParameter by dumb, CodeFragmentCodegenInfo.IParameter
data class Dumb(
@@ -0,0 +1,24 @@
package mutations
fun main() {
var a = 5
val b = 3
//Breakpoint!
val x = 0
//Breakpoint!
val y = 0
//Breakpoint!
val z = 0
}
// EXPRESSION: a = 100
// RESULT: VOID_VALUE
// EXPRESSION: b = 200
// RESULT: VOID_VALUE
// EXPRESSION: a + b
// RESULT: 300: I
@@ -0,0 +1,14 @@
LineBreakpoint created at mutations.kt:8
LineBreakpoint created at mutations.kt:11
LineBreakpoint created at mutations.kt:14
Run Java
Connected to the target VM
mutations.kt:8
Compile bytecode for a = 100
mutations.kt:11
Compile bytecode for b = 200
mutations.kt:14
Compile bytecode for a + b
Disconnected from the target VM
Process finished with exit code 0
@@ -1136,6 +1136,11 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints/multipleBreakpointsAtLine.kt");
}
@TestMetadata("mutations.kt")
public void testMutations() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints/mutations.kt");
}
@TestMetadata("nonCapturedVariables.kt")
public void testNonCapturedVariables() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints/nonCapturedVariables.kt");