Debugger: allow to evaluate kotlin expressions in java files

#KT-7549 Fixed

 If the context is inside PsiJavaFile, get list of all local variables available in current frame (this logic was removed in 29778311e8 01.12.2015 Drop unnecesary logic about additional context for lambda in debugger). For each variable create KtProperty inside top-level KtFunction in KtFile and set it as contextElement for KtCodeFragment. This file should have all imports from PsiJavaFile. We do not create properties on top-level because they will be highlighted as top-level vals/vars.
This commit is contained in:
Natalia Ukhorskaya
2016-08-01 18:34:24 +03:00
parent d823d6f4ab
commit 6a04cd39b2
19 changed files with 352 additions and 26 deletions
+1
View File
@@ -245,6 +245,7 @@ Using 'this' as function argument in constructor of non-final class
- [`KT-13059`](https://youtrack.jetbrains.com/issue/KT-13059) Fix error stepping on *Step Over* action in the end of while block
- [`KT-13037`](https://youtrack.jetbrains.com/issue/KT-13037) Fix possible deadlock in debugger in 2016.1 and exception in 2016.2
- [`KT-12651`](https://youtrack.jetbrains.com/issue/KT-12651) Fix exception in evaluate expression when bad identifier is used for marking object
- [`KT-7549`](https://youtrack.jetbrains.com/issue/KT-13037) Allow to evaluate kotlin expressions in Java files
### JS
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.psi
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.*
@@ -25,6 +26,7 @@ import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.tree.IElementType
import com.intellij.testFramework.LightVirtualFile
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.utils.addToStdlib.check
import java.util.*
@@ -41,6 +43,10 @@ abstract class KtCodeFragment(
private var viewProvider = super.getViewProvider() as SingleRootFileViewProvider
private var imports = LinkedHashSet<String>()
private val fakeContextForJavaFile: PsiElement? by lazy {
this.getCopyableUserData(FAKE_CONTEXT_FOR_JAVA_FILE)?.invoke()
}
init {
getViewProvider().forceCachedPsi(this)
init(TokenType.CODE_FRAGMENT, elementType)
@@ -67,7 +73,15 @@ abstract class KtCodeFragment(
override fun isValid() = true
override fun getContext() = context
override fun getContext(): PsiElement? {
if (fakeContextForJavaFile != null) return fakeContextForJavaFile
if (context !is KtElement) {
LOG.warn("CodeFragment with non-kotlin context should have fakeContextForJavaFile set: \noriginalContext = ${context?.getElementTextWithContext()}")
return null
}
return context
}
override fun getResolveScope() = context?.resolveScope ?: super.getResolveScope()
@@ -171,6 +185,8 @@ abstract class KtCodeFragment(
companion object {
val IMPORT_SEPARATOR: String = ","
val RUNTIME_TYPE_EVALUATOR: Key<Function1<KtExpression, KotlinType?>> = Key.create("RUNTIME_TYPE_EVALUATOR")
val ADDITIONAL_CONTEXT_FOR_LAMBDA: Key<Function0<KtElement?>> = Key.create("ADDITIONAL_CONTEXT_FOR_LAMBDA")
val FAKE_CONTEXT_FOR_JAVA_FILE: Key<Function0<KtElement>> = Key.create("FAKE_CONTEXT_FOR_JAVA_FILE")
private val LOG = Logger.getInstance(KtCodeFragment::class.java)
}
}
@@ -20,6 +20,7 @@ import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
import com.intellij.ide.highlighter.JavaFileType
@@ -37,9 +38,7 @@ import com.intellij.util.concurrency.Semaphore
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.XDebugSessionImpl
import com.intellij.xdebugger.impl.ui.tree.ValueMarkup
import com.sun.jdi.ArrayReference
import com.sun.jdi.PrimitiveValue
import com.sun.jdi.Value
import com.sun.jdi.*
import org.jetbrains.annotations.TestOnly
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.asJava.classes.KtLightClass
@@ -51,6 +50,7 @@ import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor
import org.jetbrains.kotlin.idea.refactoring.j2kText
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.versions.getKotlinJvmRuntimeMarkerClass
import org.jetbrains.kotlin.j2k.AfterConversionPass
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
@@ -117,9 +117,86 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
}
})
if (contextElement != null && contextElement !is KtElement) {
codeFragment.putCopyableUserData(KtCodeFragment.FAKE_CONTEXT_FOR_JAVA_FILE, {
val emptyFile = createFakeFileWithJavaContextElement("", contextElement)
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
val debuggerSession = debuggerContext.debuggerSession
if ((debuggerSession == null || debuggerContext.suspendContext == null) && !ApplicationManager.getApplication().isUnitTestMode) {
LOG.warn("Couldn't create fake context element for java file, debugger isn't paused on breakpoint")
return@putCopyableUserData emptyFile
}
// TODO: 'this' is unavailable
val visibleVariables = getVisibleLocalVariables(contextElement, debuggerContext)
if (visibleVariables == null) {
LOG.warn("Couldn't get a list of local variables for ${debuggerContext.sourcePosition.file.name}:${debuggerContext.sourcePosition.line}")
return@putCopyableUserData emptyFile
}
val fakeFunctionText = StringBuilder().apply {
append("fun _java_locals_debug_fun_() {\n")
val psiNameHelper = PsiNameHelper.getInstance(project)
visibleVariables.forEach {
val variable = it.key
val variableName = variable.name()
if (!psiNameHelper.isIdentifier(variableName)) return@forEach
val kotlinProperty = createKotlinProperty(project, variableName, variable.type().name(), it.value) ?: return@forEach
append("$kotlinProperty\n")
}
append("val _debug_context_val = 1\n")
append("}")
}.toString()
val fakeFile = createFakeFileWithJavaContextElement(fakeFunctionText, contextElement)
val fakeFunction = fakeFile.declarations.firstOrNull() as? KtFunction
val fakeContext = (fakeFunction?.bodyExpression as? KtBlockExpression)?.statements?.lastOrNull()
return@putCopyableUserData wrapContextIfNeeded(project, contextElement, fakeContext) ?: emptyFile
})
}
return codeFragment
}
private fun getVisibleLocalVariables(contextElement: PsiElement?, debuggerContext: DebuggerContextImpl): Map<LocalVariable, Value>? {
val semaphore = Semaphore()
semaphore.down()
var visibleVariables: Map<LocalVariable, Value>? = null
val worker = object : DebuggerCommandImpl() {
override fun action() {
try {
val frame = if (ApplicationManager.getApplication().isUnitTestMode)
contextElement?.getCopyableUserData(DEBUG_CONTEXT_FOR_TESTS)?.frameProxy?.stackFrame
else
debuggerContext.frameProxy?.stackFrame
visibleVariables = frame?.let { it.getValues(it.visibleVariables()) } ?: emptyMap<LocalVariable, Value>()
}
catch(ignored: AbsentInformationException) {
// Debug info unavailable
}
finally {
semaphore.up()
}
}
}
debuggerContext.debugProcess?.managerThread?.invoke(worker)
for (i in 0..50) {
if (semaphore.waitFor(20)) break
}
return visibleVariables
}
private fun initImports(imports: String?): String? {
if (imports != null && !imports.isEmpty()) {
return imports.split(KtCodeFragment.IMPORT_SEPARATOR)
@@ -142,8 +219,11 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
return import
}
private fun getWrappedContextElement(project: Project, context: PsiElement?)
= wrapContextIfNeeded(project, context, getContextElement(context))
private fun getWrappedContextElement(project: Project, context: PsiElement?): PsiElement? {
val newContext = getContextElement(context)
if (newContext !is KtElement) return newContext
return wrapContextIfNeeded(project, context, newContext)
}
override fun createPresentationCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment {
val kotlinCodeFragment = createCodeFragment(item, context, project)
@@ -190,11 +270,16 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
}
override fun isContextAccepted(contextElement: PsiElement?): Boolean {
if (contextElement is PsiCodeBlock) {
// PsiCodeBlock -> DummyHolder -> originalElement
return isContextAccepted(contextElement.context?.context)
return when {
// PsiCodeBlock -> DummyHolder -> originalElement
contextElement is PsiCodeBlock -> isContextAccepted(contextElement.context?.context)
contextElement == null -> false
contextElement.language == KotlinFileType.INSTANCE.language -> true
contextElement.language == JavaFileType.INSTANCE.language -> {
getKotlinJvmRuntimeMarkerClass(contextElement.project, contextElement.resolveScope) != null
}
else -> false
}
return contextElement?.language == KotlinFileType.INSTANCE.language
}
override fun getFileType() = KotlinFileType.INSTANCE
@@ -206,7 +291,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
val DEBUG_LABEL_SUFFIX: String = "_DebugLabel"
@TestOnly val DEBUG_CONTEXT_FOR_TESTS: Key<DebuggerContextImpl> = Key.create("DEBUG_CONTEXT_FOR_TESTS")
fun getContextElement(elementAt: PsiElement?): KtElement? {
fun getContextElement(elementAt: PsiElement?): PsiElement? {
if (elementAt == null) return null
if (elementAt is PsiCodeBlock) {
@@ -218,6 +303,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
}
val containingFile = elementAt.containingFile
if (containingFile is PsiJavaFile) return elementAt
if (containingFile !is KtFile) return null
// elementAt can be PsiWhiteSpace when codeFragment is created from line start offset (in case of first opening EE window)
@@ -300,11 +386,27 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
return createWrappingContext(text, labels, newContext, project)
}
private fun createFakeFileWithJavaContextElement(funWithLocalVariables: String, javaContext: PsiElement): KtFile {
val javaFile = javaContext.containingFile as? PsiJavaFile
val sb = StringBuilder()
javaFile?.packageName?.check { !it.isBlank() }?.let {
sb.append("package ").append(it.quoteIfNeeded()).append("\n")
}
javaFile?.importList?.let { sb.append(it.text).append("\n") }
sb.append(funWithLocalVariables)
return KtPsiFactory(javaContext.project).createAnalyzableFile("fakeFileForJavaContextInDebugger.kt", sb.toString(), javaContext)
}
// internal for test
fun createWrappingContext(
newFragmentText: String,
labels: Map<String, Value>,
originalContext: PsiElement?,
originalContext: KtElement?,
project: Project
): KtElement? {
val codeFragment = KtPsiFactory(project).createBlockCodeFragment(newFragmentText, originalContext)
@@ -318,6 +420,6 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
}
})
return getContextElement(codeFragment.findElementAt(codeFragment.text.length - 1))
return getContextElement(codeFragment.findElementAt(codeFragment.text.length - 1)) as? KtElement
}
}
@@ -92,20 +92,18 @@ object KotlinEvaluationBuilder: EvaluatorBuilder {
return EvaluatorBuilderImpl.getInstance()!!.build(codeFragment, position)
}
val file = position.file
if (file !is KtFile) {
throw EvaluateExceptionUtil.createEvaluateException("Couldn't evaluate kotlin expression in non-kotlin context")
}
if (position.line < 0) {
throw EvaluateExceptionUtil.createEvaluateException("Couldn't evaluate kotlin expression at $position")
}
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
if (document == null || document.lineCount < position.line) {
throw EvaluateExceptionUtil.createEvaluateException(
"Couldn't evaluate kotlin expression: breakpoint is placed outside the file. " +
"It may happen when you've changed source file after starting a debug process.")
val file = position.file
if (file is KtFile) {
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
if (document == null || document.lineCount < position.line) {
throw EvaluateExceptionUtil.createEvaluateException(
"Couldn't evaluate kotlin expression: breakpoint is placed outside the file. " +
"It may happen when you've changed source file after starting a debug process.")
}
}
if (codeFragment.context !is KtElement) {
@@ -82,7 +82,7 @@ fun getFunctionForExtractedFragment(
}
fun generateFunction(): ExtractionResult? {
val originalFile = breakpointFile as KtFile
val originalFile = codeFragment.getContextContainingFile() ?: return null
val newDebugExpressions = addDebugExpressionIntoTmpFileForExtractFunction(originalFile, codeFragment, breakpointLine)
if (newDebugExpressions.isEmpty()) return null
@@ -124,6 +124,7 @@ fun addDebugExpressionIntoTmpFileForExtractFunction(originalFile: KtFile, codeFr
val tmpFile = originalFile.copy() as KtFile
tmpFile.suppressDiagnosticsInDebugMode = true
tmpFile.analysisContext = originalFile.analysisContext
val contextElement = getExpressionToAddDebugExpressionBefore(tmpFile, codeFragment.getOriginalContext(), line) ?: return emptyList()
+12
View File
@@ -0,0 +1,12 @@
LineBreakpoint created at jcBlock.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! jcBlock.JcBlockKt
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
jcBlock.kt:6
JavaClass.java:16
JavaClass.java:18
JavaClass.java:19
Compile bytecode for bodyVal
Compile bytecode for thenVal
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
+10
View File
@@ -0,0 +1,10 @@
LineBreakpoint created at jcImports.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! jcImports.JcImportsKt
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
jcImports.kt:6
JavaClass.java:27
JavaClass.java:28
Compile bytecode for list.filter { it == 1 }.size
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
+10
View File
@@ -0,0 +1,10 @@
LineBreakpoint created at jcLocalVariable.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! jcLocalVariable.JcLocalVariableKt
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
jcLocalVariable.kt:6
JavaClass.java:11
JavaClass.java:12
Compile bytecode for i
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
+11
View File
@@ -0,0 +1,11 @@
LineBreakpoint created at jcMarkedObject.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! jcMarkedObject.JcMarkedObjectKt
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
jcMarkedObject.kt:6
JavaClass.java:39
JavaClass.java:40
Compile bytecode for i
Compile bytecode for i_DebugLabel
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
+9
View File
@@ -0,0 +1,9 @@
LineBreakpoint created at jcSimple.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! jcSimple.JcSimpleKt
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
jcSimple.kt:6
JavaClass.java:7
Compile bytecode for 1 + 1
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,19 @@
package jcBlock
fun main(args: Array<String>) {
val javaClass = forTests.javaContext.JavaClass()
//Breakpoint!
javaClass.block()
}
// STEP_INTO: 1
// STEP_OVER: 2
// EXPRESSION: bodyVal
// RESULT: 1: I
// EXPRESSION: thenVal
// RESULT: 1: I
// EXPRESSION: elseVal
// RESULT: Unresolved reference: elseVal
@@ -0,0 +1,13 @@
package jcImports
fun main(args: Array<String>) {
val javaClass = forTests.javaContext.JavaClass()
//Breakpoint!
javaClass.imports()
}
// STEP_INTO: 1
// STEP_OVER: 1
// EXPRESSION: list.filter { it == 1 }.size
// RESULT: 1: I
@@ -0,0 +1,13 @@
package jcLocalVariable
fun main(args: Array<String>) {
val javaClass = forTests.javaContext.JavaClass()
//Breakpoint!
javaClass.localVariable()
}
// STEP_INTO: 1
// STEP_OVER: 1
// EXPRESSION: i
// RESULT: 1: I
@@ -0,0 +1,18 @@
package jcMarkedObject
fun main(args: Array<String>) {
val javaClass = forTests.javaContext.JavaClass()
//Breakpoint!
javaClass.markObject()
}
// STEP_INTO: 1
// STEP_OVER: 1
// EXPRESSION: i
// RESULT: instance of java.lang.Integer(id=ID): Ljava/lang/Integer;
// EXPRESSION: i_DebugLabel
// RESULT: instance of java.lang.Integer(id=ID): Ljava/lang/Integer;
// DEBUG_LABEL: i = i
@@ -0,0 +1,12 @@
package jcSimple
fun main(args: Array<String>) {
val javaClass = forTests.javaContext.JavaClass()
//Breakpoint!
javaClass.simple()
}
// STEP_INTO: 1
// EXPRESSION: 1 + 1
// RESULT: 2: I
@@ -0,0 +1,42 @@
package forTests.javaContext;
import java.util.ArrayList;
public class JavaClass {
public void simple() {
int breakpoint = 1;
}
public void localVariable() {
int i = 1;
int breakpoint = 1;
}
public void block() {
int bodyVal = 1;
if (true) {
int thenVal = 1;
int breakpoint = 1;
}
else {
int elseVal = 1;
}
}
public void imports() {
ArrayList<Integer> list = createList();
int breakpoint = 1;
}
private ArrayList<Integer> createList() {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
return list;
}
public void markObject() {
Integer i = 1;
int breakpoint = 1;
}
}
@@ -137,7 +137,7 @@ public abstract class KotlinDebuggerTestCase extends DescriptorTestCase {
MockLibraryUtil.compileKotlin(sourcesDir, outDir, CUSTOM_LIBRARY_JAR.getPath());
List<String> options = Arrays.asList("-d", outputDirPath, "-classpath", ForTestCompileRuntime.runtimeJarForTests().getPath());
List<String> options = Arrays.asList("-d", outputDirPath, "-classpath", ForTestCompileRuntime.runtimeJarForTests().getPath(), "-g");
try {
KotlinTestUtils.compileJavaFiles(findJavaFiles(new File(sourcesDir)), options);
}
@@ -697,6 +697,45 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
}
}
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class JavaContext extends AbstractKotlinEvaluateExpressionTest {
public void testAllFilesPresentInJavaContext() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("jcBlock.kt")
public void testJcBlock() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext/jcBlock.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("jcImports.kt")
public void testJcImports() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext/jcImports.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("jcLocalVariable.kt")
public void testJcLocalVariable() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext/jcLocalVariable.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("jcMarkedObject.kt")
public void testJcMarkedObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext/jcMarkedObject.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("jcSimple.kt")
public void testJcSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/javaContext/jcSimple.kt");
doSingleBreakpointTest(fileName);
}
}
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/labels")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)