Debugger: Implement error reporting for evaluator

This commit is contained in:
Yan Zhulanow
2019-05-27 18:36:42 +09:00
parent 8a5fd44a55
commit 7e564e94ed
6 changed files with 282 additions and 48 deletions
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
@@ -50,6 +51,7 @@ 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 org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerEvaluator
import java.lang.reflect.Modifier
import java.util.*
@@ -60,6 +62,13 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
private val kotlinEvaluator by lazy {
val debugProcess = descriptor.debugProcess as DebugProcessImpl // Cast as in JavaStackFrame
KotlinDebuggerEvaluator(debugProcess, this@KotlinStackFrame)
}
override fun getEvaluator() = kotlinEvaluator
override fun superBuildVariables(evaluationContext: EvaluationContextImpl, children: XValueChildrenList) {
if (!kotlinVariableViewService.kotlinVariableView) {
return super.superBuildVariables(evaluationContext, children)
@@ -68,7 +68,7 @@ class KotlinCodeFragmentFactory : CodeFragmentFactory() {
}
val codeFragment = constructor(project, "fragment.kt", item.text, initImports(item.imports), contextElement)
supplyDebugLabels(codeFragment, context)
supplyDebugInformation(item, codeFragment, context)
codeFragment.putCopyableUserData(KtCodeFragment.RUNTIME_TYPE_EVALUATOR, { expression: KtExpression ->
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
@@ -139,10 +139,16 @@ class KotlinCodeFragmentFactory : CodeFragmentFactory() {
return codeFragment
}
private fun supplyDebugLabels(codeFragment: KtCodeFragment, context: PsiElement?) {
private fun supplyDebugInformation(item: TextWithImports, codeFragment: KtCodeFragment, context: PsiElement?) {
val project = codeFragment.project
val debugProcess = getDebugProcess(project, context) ?: return
DebugLabelPropertyDescriptorProvider(codeFragment, debugProcess).supplyDebugLabels()
val evaluator = debugProcess.session.xDebugSession?.currentStackFrame?.evaluator
if (evaluator is KotlinDebuggerEvaluator) {
codeFragment.putUserData(EVALUATION_TYPE, evaluator.getType(item))
}
}
private fun getDebugProcess(project: Project, context: PsiElement?): DebugProcessImpl? {
@@ -295,6 +301,8 @@ class KotlinCodeFragmentFactory : CodeFragmentFactory() {
@get:TestOnly
val DEBUG_CONTEXT_FOR_TESTS: Key<DebuggerContextImpl> = Key.create("DEBUG_CONTEXT_FOR_TESTS")
val EVALUATION_TYPE: Key<KotlinDebuggerEvaluator.EvaluationType> = Key.create("DEBUG_EVALUATION_TYPE")
const val FAKE_JAVA_CONTEXT_FUNCTION_NAME = "_java_locals_debug_fun_"
const val FAKE_JAVA_THIS_NAME = "\$this\$_java_locals_debug_fun_"
@@ -22,6 +22,7 @@ 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.*
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.diagnostic.Attachment
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProcessCanceledException
@@ -42,6 +43,7 @@ import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.diagnostics.Severity
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.core.util.attachmentByPsiFile
import org.jetbrains.kotlin.idea.core.util.mergeAttachments
import org.jetbrains.kotlin.idea.core.util.runInReadActionWithWriteActionPriorityWithPCE
@@ -65,6 +67,8 @@ import org.jetbrains.kotlin.resolve.isInlineClassType
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.kotlin.idea.debugger.evaluate.EvaluationStatus.EvaluationContextLanguage
import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.ClassLoadingResult
import java.util.*
internal val LOG = Logger.getInstance(KotlinEvaluator::class.java)
@@ -93,35 +97,68 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
return context.debugProcess.virtualMachineProxy.mirrorOfVoid()
}
val status = EvaluationStatus()
val evaluationType = codeFragment.getUserData(KotlinCodeFragmentFactory.EVALUATION_TYPE)
if (evaluationType != null) {
status.evaluationType(evaluationType)
}
val language = when (codeFragment.language) {
KotlinLanguage.INSTANCE -> EvaluationContextLanguage.Kotlin
JavaLanguage.INSTANCE -> EvaluationContextLanguage.Java
else -> EvaluationContextLanguage.Other
}
status.contextLanguage(language)
try {
return evaluateWithStatus(context, status)
} finally {
status.send()
}
}
private fun evaluateWithStatus(context: EvaluationContextImpl, status: EvaluationStatus): Any? {
if (DumbService.getInstance(codeFragment.project).isDumb) {
evaluationException("Code fragment evaluation is not available in the dumb mode")
}
val frameProxy = context.frameProxy
?: evaluationException("Cannot evaluate a code fragment: frame proxy is not available")
val frameProxy = context.frameProxy ?: run {
status.error(EvaluationError.NoFrameProxy)
evaluationException("Cannot evaluate a code fragment: frame proxy is not available")
}
val operatingThread = context.suspendContext.thread
?: evaluationException("Cannot evaluate a code fragment: thread is not available")
val operatingThread = context.suspendContext.thread ?: run {
status.error(EvaluationError.ThreadNotAvailable)
evaluationException("Cannot evaluate a code fragment: thread is not available")
}
if (!operatingThread.isSuspended) {
status.error(EvaluationError.ThreadNotSuspended)
evaluationException("Evaluation is available only for the suspended threads")
}
try {
val executionContext = ExecutionContext(context, frameProxy)
return evaluateSafe(executionContext)
return evaluateSafe(executionContext, status)
} catch (e: EvaluateException) {
status.error(EvaluationError.EvaluateException)
throw e
} catch (e: ProcessCanceledException) {
status.error(EvaluationError.ProcessCancelledException)
evaluationException(e)
} catch (e: Eval4JInterpretingException) {
status.error(EvaluationError.InterpretingException)
evaluationException(e.cause)
} catch (e: Exception) {
val isSpecialException = isSpecialException(e)
if (isSpecialException) {
status.error(EvaluationError.SpecialException)
evaluationException(e)
}
status.error(EvaluationError.GenericException)
reportError(codeFragment, sourcePosition, e.message ?: "An exception occurred", e)
val cause = if (e.message != null) ": ${e.message}" else ""
@@ -129,38 +166,44 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
}
}
private fun evaluateSafe(context: ExecutionContext): Any? {
fun compilerFactory(): CompiledDataDescriptor = compileCodeFragment(context)
private fun evaluateSafe(context: ExecutionContext, status: EvaluationStatus): Any? {
fun compilerFactory(): CompiledDataDescriptor = compileCodeFragment(context, status)
val (compiledData, isCompiledDataFromCache) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory)
val classLoaderRef = loadClassesSafely(context, compiledData.classes)
val (compiledData, _) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory, force = false)
val classLoadingResult = loadClassesSafely(context, compiledData.classes)
val classLoaderRef = (classLoadingResult as? ClassLoadingResult.Success)?.classLoader
if (classLoadingResult is ClassLoadingResult.Failure) {
status.classLoadingFailed()
}
val result = if (classLoaderRef != null) {
evaluateWithCompilation(context, compiledData, classLoaderRef)
?: evaluateWithEval4J(context, compiledData, classLoaderRef)
try {
status.usedEvaluator(EvaluationStatus.EvaluatorType.Bytecode)
return evaluateWithCompilation(context, compiledData, classLoaderRef)
} catch (e: Throwable) {
status.compilingEvaluatorFailed()
LOG.warn("Compiling evaluator failed", e)
status.usedEvaluator(EvaluationStatus.EvaluatorType.Eval4j)
evaluateWithEval4J(context, compiledData, classLoaderRef)
}
} else {
status.usedEvaluator(EvaluationStatus.EvaluatorType.Eval4j)
evaluateWithEval4J(context, compiledData, classLoaderRef)
}
// If bytecode was taken from cache and exception was thrown - recompile bytecode and run eval4j again
if (isCompiledDataFromCache && result is ExceptionThrown && result.kind == ExceptionThrown.ExceptionKind.BROKEN_CODE) {
val (recompiledData, _) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory, force = true)
return evaluateWithEval4J(context, recompiledData, classLoaderRef).toJdiValue(context)
}
return when (result) {
is InterpreterResult -> result.toJdiValue(context)
else -> result
}
return result.toJdiValue(context)
}
private fun compileCodeFragment(context: ExecutionContext): CompiledDataDescriptor {
private fun compileCodeFragment(context: ExecutionContext, status: EvaluationStatus): CompiledDataDescriptor {
val debugProcess = context.debugProcess
var analysisResult = checkForErrors(codeFragment, debugProcess)
var analysisResult = analyze(codeFragment, status, debugProcess)
if (codeFragment.wrapToStringIfNeeded(analysisResult.bindingContext)) {
// Repeat analysis with toString() added
analysisResult = checkForErrors(codeFragment, debugProcess)
analysisResult = analyze(codeFragment, status, debugProcess)
}
val (bindingContext) = runReadAction {
@@ -172,8 +215,13 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
val moduleDescriptor = analysisResult.moduleDescriptor
val result = CodeFragmentCompiler(context).compile(codeFragment, bindingContext, moduleDescriptor)
return createCompiledDataDescriptor(result, sourcePosition)
try {
val result = CodeFragmentCompiler(context).compile(codeFragment, bindingContext, moduleDescriptor)
return createCompiledDataDescriptor(result, sourcePosition)
} catch (e: Throwable) {
status.error(EvaluationError.BackendException)
throw e
}
}
private fun KtCodeFragment.wrapToStringIfNeeded(bindingContext: BindingContext): Boolean {
@@ -205,7 +253,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
val files: List<KtFile>
)
private fun checkForErrors(codeFragment: KtCodeFragment, debugProcess: DebugProcessImpl): ErrorCheckingResult {
private fun analyze(codeFragment: KtCodeFragment, status: EvaluationStatus, debugProcess: DebugProcessImpl): ErrorCheckingResult {
return runInReadActionWithWriteActionPriorityWithPCE {
try {
AnalyzingUtils.checkForSyntacticErrors(codeFragment)
@@ -221,6 +269,7 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
val analysisResult = resolutionFacade.analyzeWithAllCompilerChecks(filesToAnalyze)
if (analysisResult.isError()) {
status.error(EvaluationError.FrontendException)
evaluationException(analysisResult.error)
}
@@ -229,7 +278,10 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
bindingContext.diagnostics
.filter { it.factory !in IGNORED_DIAGNOSTICS }
.firstOrNull { it.severity == Severity.ERROR && it.psiElement.containingFile == codeFragment }
?.let { evaluationException(DefaultErrorMessages.render(it)) }
?.let {
status.error(EvaluationError.ErrorsInCode)
evaluationException(DefaultErrorMessages.render(it))
}
ErrorCheckingResult(bindingContext, analysisResult.moduleDescriptor, Collections.singletonList(codeFragment))
}
@@ -240,17 +292,12 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
compiledData: CompiledDataDescriptor,
classLoader: ClassLoaderReference
): Value? {
return try {
runEvaluation(context, compiledData, classLoader) { args ->
val mainClassType = context.findClass(GENERATED_CLASS_NAME, classLoader) as? ClassType
?: error("Can not find class \"$GENERATED_CLASS_NAME\"")
val mainMethod = mainClassType.methods().single { it.name() == GENERATED_FUNCTION_NAME }
val returnValue = context.invokeMethod(mainClassType, mainMethod, args)
EvaluatorValueConverter(context).unref(returnValue)
}
} catch (e: Throwable) {
LOG.error("Unable to evaluate the expression with compilation", e)
return null
return runEvaluation(context, compiledData, classLoader) { args ->
val mainClassType = context.findClass(GENERATED_CLASS_NAME, classLoader) as? ClassType
?: error("Can not find class \"$GENERATED_CLASS_NAME\"")
val mainMethod = mainClassType.methods().single { it.name() == GENERATED_FUNCTION_NAME }
val returnValue = context.invokeMethod(mainClassType, mainMethod, args)
EvaluatorValueConverter(context).unref(returnValue)
}
}
@@ -23,21 +23,32 @@ import org.jetbrains.kotlin.idea.debugger.evaluate.LOG
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoadingAdapter
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
fun loadClassesSafely(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference? {
sealed class ClassLoadingResult {
class Success(val classLoader: ClassLoaderReference) : ClassLoadingResult()
class Failure(val error: Throwable) : ClassLoadingResult()
object NotNeeded : ClassLoadingResult()
}
fun loadClassesSafely(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoadingResult {
if (classes.isEmpty()) {
return ClassLoadingResult.NotNeeded
}
return try {
loadClasses(context, classes)
val cl = loadClasses(context, classes)
if (cl != null) {
ClassLoadingResult.Success(cl)
} else {
ClassLoadingResult.NotNeeded
}
} catch (e: EvaluateException) {
throw e
} catch (e: Throwable) {
LOG.debug("Failed to evaluate expression", e)
null
ClassLoadingResult.Failure(e)
}
}
fun loadClasses(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference? {
if (classes.isEmpty()) {
return null
}
return ClassLoadingAdapter.loadClasses(context, classes)
}
@@ -0,0 +1,83 @@
/*
* 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.evaluate
import org.jetbrains.kotlin.idea.statistics.FUSEventGroups
import org.jetbrains.kotlin.idea.statistics.KotlinFUSLogger
class EvaluationStatus {
private var error: EvaluationError? = null
private var values = mutableMapOf<String, String>()
private var evaluationTypeSet: Boolean = false
fun error(kind: EvaluationError) {
if (error == null) {
error = kind
}
}
fun flag(name: String, value: Boolean) {
values[name] = value.toString()
}
fun <T : Enum<T>> value(name: String, value: T) {
values[name] = value.name
}
fun evaluationType(type: KotlinDebuggerEvaluator.EvaluationType) {
evaluationTypeSet = true
value("evaluationType", type)
}
fun send() {
if (!evaluationTypeSet) {
return
}
val statusName = error?.name ?: "Success"
KotlinFUSLogger.log(FUSEventGroups.DebugEval, statusName, values)
}
enum class EvaluatorType {
Bytecode, Eval4j
}
enum class EvaluationContextLanguage {
Java, Kotlin, Other
}
}
enum class EvaluationError {
NoFrameProxy,
ThreadNotAvailable,
ThreadNotSuspended,
ProcessCancelledException,
InterpretingException,
EvaluateException,
SpecialException,
GenericException,
FrontendException,
BackendException,
ErrorsInCode
}
fun EvaluationStatus.classLoadingFailed() {
flag("classLoadingFailed", true)
}
fun EvaluationStatus.compilingEvaluatorFailed() {
flag("compilingEvaluatorFailed", true)
}
fun EvaluationStatus.usedEvaluator(evaluator: EvaluationStatus.EvaluatorType) {
value("evaluator", evaluator)
}
fun EvaluationStatus.contextLanguage(language: EvaluationStatus.EvaluationContextLanguage) {
value("contextLanguage", language)
}
@@ -0,0 +1,76 @@
/*
* 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.evaluate
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaDebuggerEvaluator
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.openapi.util.text.StringUtil
import com.intellij.xdebugger.XExpression
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.frame.XValue
import com.intellij.xdebugger.impl.evaluate.quick.XValueHint
import com.intellij.xdebugger.impl.ui.tree.nodes.EvaluatingExpressionRootNode
import com.intellij.xdebugger.impl.ui.tree.nodes.WatchNodeImpl
import java.util.*
private data class Key(val text: String, val imports: String) {
constructor(expr: XExpression) : this(expr.expression, StringUtil.notNullize(expr.customInfo))
constructor(text: TextWithImports) : this(text.text, text.imports)
}
class KotlinDebuggerEvaluator(
debugProcess: DebugProcessImpl?,
stackFrame: JavaStackFrame?
) : JavaDebuggerEvaluator(debugProcess, stackFrame) {
private val types = HashMap<Key, EvaluationType>()
fun getType(text: TextWithImports): EvaluationType {
return types[Key(text)] ?: EvaluationType.UNKNOWN
}
override fun evaluate(expression: XExpression, callback: XEvaluationCallback, expressionPosition: XSourcePosition?) {
val key = Key(expression)
val type = getType(callback)
if (type != null) {
types[key] = type
}
val wrappedCallback = object : XEvaluationCallback {
override fun errorOccurred(errorMessage: String) {
types.remove(key)
callback.errorOccurred(errorMessage)
}
override fun evaluated(result: XValue) {
types.remove(key)
callback.evaluated(result)
}
}
super.evaluate(expression, wrappedCallback, expressionPosition)
}
private fun getType(callback: XEvaluationCallback): EvaluationType? {
val name = callback.javaClass.name
for (value in EvaluationType.values()) {
val clazz = value.clazz ?: continue
if (name.startsWith(clazz.name)) {
return value
}
}
return null
}
enum class EvaluationType(val clazz: Class<*>?) {
WATCH(WatchNodeImpl::class.java),
WINDOW(EvaluatingExpressionRootNode::class.java),
POPUP(XValueHint::class.java),
UNKNOWN(null);
}
}