Debugger: Fix lvalue evaluation (KT-11663, KT-19980)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+17
-9
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+27
-6
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
+5
-4
@@ -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)
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
|
||||
+52
-21
@@ -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> {
|
||||
|
||||
+2
-1
@@ -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
|
||||
+14
@@ -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
|
||||
Generated
+5
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user