Do not issue exceptions for non-captured local variables

This partly fixes KT-28192.
This commit is contained in:
Yan Zhulanow
2018-12-21 17:28:23 +03:00
parent 4ae01badff
commit 6cd980765c
10 changed files with 255 additions and 57 deletions
+1
View File
@@ -9,6 +9,7 @@
<w>parceler</w>
<w>repl</w>
<w>uast</w>
<w>unbox</w>
<w>unboxed</w>
<w>unmute</w>
</words>
@@ -223,7 +223,8 @@ class KotlinDebuggerCaches(project: Project) {
data class CompiledDataDescriptor(
val classes: List<ClassToLoad>,
val sourcePosition: SourcePosition,
val parameters: List<Parameter>
val parameters: List<Parameter>,
val variablesCrossingInlineBounds: Set<String>
)
data class Parameter(val callText: String, val type: KotlinType, val value: Value? = null, val error: EvaluateException? = null)
@@ -224,13 +224,15 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
companion object {
private fun extractAndCompile(codeFragment: KtCodeFragment, sourcePosition: SourcePosition, context: EvaluationContextImpl): CompiledDataDescriptor {
val (bindingContext) = codeFragment.checkForErrors()
var bindingContext = codeFragment.checkForErrors().bindingContext
if (codeFragment.wrapToStringIfNeeded(bindingContext)) {
// Repeat analysis with toString() added
codeFragment.checkForErrors()
bindingContext = codeFragment.checkForErrors().bindingContext
}
val variablesCrossingInlineBounds = ScopeCheckerForEvaluator.checkScopes(bindingContext, codeFragment)
val extractionResult = getFunctionForExtractedFragment(codeFragment, sourcePosition.file, sourcePosition.line)
?: throw IllegalStateException("Code fragment cannot be extracted to function: ${codeFragment.text}")
val (parametersDescriptor, extractedFunction) = try {
@@ -259,9 +261,11 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
val additionalFiles = outputFiles.map { ClassToLoad(getClassName(it.relativePath), it.relativePath, it.asByteArray()) }
return CompiledDataDescriptor(
additionalFiles,
sourcePosition,
parametersDescriptor)
additionalFiles,
sourcePosition,
parametersDescriptor,
variablesCrossingInlineBounds
)
}
private fun KtCodeFragment.wrapToStringIfNeeded(bindingContext: BindingContext): Boolean {
@@ -326,7 +330,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
// Prepare the main class
val argumentTypes = Type.getArgumentTypes(methodToInvoke.desc)
val args = context.getArgumentsForEvaluation(compiledData.parameters, argumentTypes)
val args = context.getArgumentsForEvaluation(compiledData.parameters, argumentTypes, compiledData)
.zip(argumentTypes)
.map { (value, type) ->
// Make argument type classes prepared for sure
@@ -359,7 +363,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
// Maybe just take the single method from the class, as it is done in 'evaluateWithCompilation'
if (name == GENERATED_FUNCTION_NAME || name.startsWith(GENERATED_FUNCTION_NAME + "-")) {
val argumentTypes = Type.getArgumentTypes(desc)
val args = context.getArgumentsForEvaluation(compiledData.parameters, argumentTypes)
val args = context.getArgumentsForEvaluation(compiledData.parameters, argumentTypes, compiledData)
return object : MethodNode(Opcodes.API_VERSION, access, name, desc, signature, exceptions) {
override fun visitEnd() {
@@ -507,18 +511,28 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
private fun EvaluationContextImpl.getArgumentsForEvaluation(
parameters: List<Parameter>,
parameterTypes: Array<Type>
parameterTypes: Array<Type>,
compiledData: CompiledDataDescriptor
): List<Value> {
val variableFinder = VariableFinder.instance(this) ?: error("No stack frame available")
return parameters.zip(parameterTypes).map { (parameter, type) ->
parameter.error?.let { throw it }
parameter.value?.let { return@map it }
val result = parameter.value ?: variableFinder.get(parameter.callText, type).asValue()
val name = parameter.callText
val result = variableFinder.find(name, type)
if (LOG.isDebugEnabled) {
LOG.debug("Parameter for eval4j: name = ${parameter.callText}, type = $type, value = $result")
if (result == null) {
if (name in compiledData.variablesCrossingInlineBounds) {
throw EvaluateExceptionUtil.createEvaluateException("'$name' is not captured")
} else {
throw VariableFinder.variableNotFound(this, buildString {
append("Cannot find local variable: name = '").append(name).append("', type = ").append(type.className)
})
}
} else {
return@map result.value.asValue()
}
result
}
}
@@ -0,0 +1,72 @@
/*
* 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.psi.PsiElement
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.kotlin.toSourceElement
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.source.getPsi
object ScopeCheckerForEvaluator {
fun checkScopes(bindingContext: BindingContext, codeFragment: KtCodeFragment): Set<String> {
val result = hashSetOf<String>()
codeFragment.accept(object : KtTreeVisitor<Unit>() {
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression, data: Unit?): Void? {
val target = bindingContext[BindingContext.REFERENCE_TARGET, expression]
if (target is DeclarationDescriptorWithVisibility && target.visibility == Visibilities.LOCAL) {
val declarationPsiElement = target.toSourceElement.getPsi()
if (declarationPsiElement != null) {
runReadAction {
if (doesCrossInlineBounds(bindingContext, expression, declarationPsiElement)) {
result.add(expression.getReferencedName())
}
}
}
}
return null
}
}, Unit)
return result
}
private fun doesCrossInlineBounds(bindingContext: BindingContext, reference: KtSimpleNameExpression, declaration: PsiElement): Boolean {
val declarationParent = declaration.parent ?: return false
var currentParent: PsiElement? = reference.parent?.takeIf { it.isInside(declarationParent) } ?: return false
while (currentParent != null && currentParent != declarationParent) {
if (currentParent is KtFunctionLiteral) {
val functionDescriptor = bindingContext[BindingContext.FUNCTION, currentParent]
if (functionDescriptor != null && !functionDescriptor.isInline) {
return true
}
}
currentParent = when (currentParent) {
is KtCodeFragment -> currentParent.context
else -> currentParent.parent
}
}
return false
}
private tailrec fun PsiElement.isInside(parent: PsiElement): Boolean {
if (parent.isAncestor(this)) {
return true
}
val context = (this.containingFile as? KtCodeFragment)?.context ?: return false
return context.isInside(parent)
}
}
@@ -14,6 +14,7 @@ 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.coroutines.CONTINUATION_VARIABLE_NAME
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
@@ -50,7 +51,40 @@ class VariableFinder private constructor(private val context: EvaluationContextI
return NamedEntity("<nameForUnwrapOnly>", value.type()) { value }.unwrap().value()
}
private val inlinedThisRegex = getLocalVariableNameRegexInlineAware(AsmUtil.THIS + "_")
fun variableNotFound(context: EvaluationContextImpl, message: String): Exception {
val frameProxy = context.frameProxy
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)
}
// org.jetbrains.kotlin.codegen.inline.MethodInliner.prepareNode
private const val OUTER_THIS_FOR_INLINE = AsmUtil.THIS + '_'
val inlinedThisRegex = getLocalVariableNameRegexInlineAware(OUTER_THIS_FOR_INLINE)
private fun getCapturedVariableNameRegex(capturedName: String): Regex {
val escapedName = Regex.escape(capturedName)
@@ -168,17 +202,6 @@ class VariableFinder private constructor(private val context: EvaluationContextI
}
}
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 + "@") -> {
@@ -317,7 +340,7 @@ class VariableFinder private constructor(private val context: EvaluationContextI
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
|| inlinedThisRegex.matches(name)
if (kind is VariableKind.LabeledThis) {
variables.namedEntitySequence()
@@ -366,35 +389,6 @@ class VariableFinder private constructor(private val context: EvaluationContextI
|| 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,79 @@
package nonCapturedVariables
fun main() {
val a = 5
inlineBlock {
//Breakpoint!
val b = 6
inlineBlock {
//Breakpoint!
val c = 7
block {
//Breakpoint!
val d = 8
inlineBlock {
//Breakpoint!
val e = 9
block {
//Breakpoint!
val f = 10
block {
//Breakpoint!
val g = 11
//Breakpoint!
val g2 = 12
//Breakpoint!
val g3 = 13
//Breakpoint!
val g4 = 14
}
}
}
}
}
}
}
private fun block(block: () -> Unit) {
block()
}
private inline fun inlineBlock(block: () -> Unit) {
block()
}
// EXPRESSION: a
// RESULT: 5: I
// EXPRESSION: b
// RESULT: 6: I
// EXPRESSION: c
// RESULT: 'c' is not captured
// EXPRESSION: d
// RESULT: 8: I
// EXPRESSION: e
// RESULT: 'e' is not captured
// EXPRESSION: f
// RESULT: 'f' is not captured
// EXPRESSION: e
// RESULT: 'e' is not captured
// EXPRESSION: d
// RESULT: 'd' is not captured
// EXPRESSION: c
// RESULT: 'c' is not captured
@@ -0,0 +1,32 @@
LineBreakpoint created at nonCapturedVariables.kt:8
LineBreakpoint created at nonCapturedVariables.kt:12
LineBreakpoint created at nonCapturedVariables.kt:16
LineBreakpoint created at nonCapturedVariables.kt:20
LineBreakpoint created at nonCapturedVariables.kt:24
LineBreakpoint created at nonCapturedVariables.kt:28
LineBreakpoint created at nonCapturedVariables.kt:31
LineBreakpoint created at nonCapturedVariables.kt:34
LineBreakpoint created at nonCapturedVariables.kt:37
Run Java
Connected to the target VM
nonCapturedVariables.kt:8
Compile bytecode for a
nonCapturedVariables.kt:12
Compile bytecode for b
nonCapturedVariables.kt:16
Compile bytecode for c
nonCapturedVariables.kt:20
Compile bytecode for d
nonCapturedVariables.kt:24
Compile bytecode for e
nonCapturedVariables.kt:28
Compile bytecode for f
nonCapturedVariables.kt:31
Compile bytecode for e
nonCapturedVariables.kt:34
Compile bytecode for d
nonCapturedVariables.kt:37
Compile bytecode for c
Disconnected from the target VM
Process finished with exit code 0
@@ -15,4 +15,4 @@ fun foo(f: () -> Unit) {
// PRINT_FRAME
// EXPRESSION: val1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'val1', type = int
// RESULT: 'val1' is not captured
@@ -20,7 +20,7 @@ fun foo(f: () -> Unit) {
// PRINT_FRAME
// EXPRESSION: val1
// RESULT: java.lang.AssertionError : Cannot find local variable: name = 'val1', type = int
// RESULT: 'val1' is not captured
frame = invoke:7, FrameLambdaNotUsedKt$main$1 {frameLambdaNotUsed}
this = this = {frameLambdaNotUsed.FrameLambdaNotUsedKt$main$1@uniqueID}Function0<kotlin.Unit>
field = arity: int = 0 (sp = Lambda.!EXT!)
@@ -996,6 +996,11 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints/multipleBreakpointsAtLine.kt");
}
@TestMetadata("nonCapturedVariables.kt")
public void testNonCapturedVariables() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints/nonCapturedVariables.kt");
}
@TestMetadata("privateMembersPriority.kt")
public void testPrivateMembersPriority() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints/privateMembersPriority.kt");