Skip to content

Commit 6a04cd3

Browse files
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 2977831 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.
1 parent d823d6f commit 6a04cd3

File tree

19 files changed

+352
-26
lines changed

19 files changed

+352
-26
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ Using 'this' as function argument in constructor of non-final class
245245
- [`KT-13059`](https://youtrack.jetbrains.com/issue/KT-13059) Fix error stepping on *Step Over* action in the end of while block
246246
- [`KT-13037`](https://youtrack.jetbrains.com/issue/KT-13037) Fix possible deadlock in debugger in 2016.1 and exception in 2016.2
247247
- [`KT-12651`](https://youtrack.jetbrains.com/issue/KT-12651) Fix exception in evaluate expression when bad identifier is used for marking object
248+
- [`KT-7549`](https://youtrack.jetbrains.com/issue/KT-13037) Allow to evaluate kotlin expressions in Java files
248249

249250
### JS
250251

compiler/frontend/src/org/jetbrains/kotlin/psi/KtCodeFragment.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.jetbrains.kotlin.psi
1818

19+
import com.intellij.openapi.diagnostic.Logger
1920
import com.intellij.openapi.project.Project
2021
import com.intellij.openapi.util.Key
2122
import com.intellij.psi.*
@@ -25,6 +26,7 @@ import com.intellij.psi.search.GlobalSearchScope
2526
import com.intellij.psi.tree.IElementType
2627
import com.intellij.testFramework.LightVirtualFile
2728
import org.jetbrains.kotlin.idea.KotlinFileType
29+
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
2830
import org.jetbrains.kotlin.types.KotlinType
2931
import org.jetbrains.kotlin.utils.addToStdlib.check
3032
import java.util.*
@@ -41,6 +43,10 @@ abstract class KtCodeFragment(
4143
private var viewProvider = super.getViewProvider() as SingleRootFileViewProvider
4244
private var imports = LinkedHashSet<String>()
4345

46+
private val fakeContextForJavaFile: PsiElement? by lazy {
47+
this.getCopyableUserData(FAKE_CONTEXT_FOR_JAVA_FILE)?.invoke()
48+
}
49+
4450
init {
4551
getViewProvider().forceCachedPsi(this)
4652
init(TokenType.CODE_FRAGMENT, elementType)
@@ -67,7 +73,15 @@ abstract class KtCodeFragment(
6773

6874
override fun isValid() = true
6975

70-
override fun getContext() = context
76+
override fun getContext(): PsiElement? {
77+
if (fakeContextForJavaFile != null) return fakeContextForJavaFile
78+
if (context !is KtElement) {
79+
LOG.warn("CodeFragment with non-kotlin context should have fakeContextForJavaFile set: \noriginalContext = ${context?.getElementTextWithContext()}")
80+
return null
81+
}
82+
83+
return context
84+
}
7185

7286
override fun getResolveScope() = context?.resolveScope ?: super.getResolveScope()
7387

@@ -171,6 +185,8 @@ abstract class KtCodeFragment(
171185
companion object {
172186
val IMPORT_SEPARATOR: String = ","
173187
val RUNTIME_TYPE_EVALUATOR: Key<Function1<KtExpression, KotlinType?>> = Key.create("RUNTIME_TYPE_EVALUATOR")
174-
val ADDITIONAL_CONTEXT_FOR_LAMBDA: Key<Function0<KtElement?>> = Key.create("ADDITIONAL_CONTEXT_FOR_LAMBDA")
188+
val FAKE_CONTEXT_FOR_JAVA_FILE: Key<Function0<KtElement>> = Key.create("FAKE_CONTEXT_FOR_JAVA_FILE")
189+
190+
private val LOG = Logger.getInstance(KtCodeFragment::class.java)
175191
}
176192
}

idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.intellij.debugger.DebuggerManagerEx
2020
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory
2121
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
2222
import com.intellij.debugger.engine.evaluation.TextWithImports
23+
import com.intellij.debugger.engine.events.DebuggerCommandImpl
2324
import com.intellij.debugger.impl.DebuggerContextImpl
2425
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
2526
import com.intellij.ide.highlighter.JavaFileType
@@ -37,9 +38,7 @@ import com.intellij.util.concurrency.Semaphore
3738
import com.intellij.xdebugger.XDebuggerManager
3839
import com.intellij.xdebugger.impl.XDebugSessionImpl
3940
import com.intellij.xdebugger.impl.ui.tree.ValueMarkup
40-
import com.sun.jdi.ArrayReference
41-
import com.sun.jdi.PrimitiveValue
42-
import com.sun.jdi.Value
41+
import com.sun.jdi.*
4342
import org.jetbrains.annotations.TestOnly
4443
import org.jetbrains.eval4j.jdi.asValue
4544
import org.jetbrains.kotlin.asJava.classes.KtLightClass
@@ -51,6 +50,7 @@ import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor
5150
import org.jetbrains.kotlin.idea.refactoring.j2kText
5251
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
5352
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
53+
import org.jetbrains.kotlin.idea.versions.getKotlinJvmRuntimeMarkerClass
5454
import org.jetbrains.kotlin.j2k.AfterConversionPass
5555
import org.jetbrains.kotlin.psi.*
5656
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
@@ -117,9 +117,86 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
117117
}
118118
})
119119

120+
if (contextElement != null && contextElement !is KtElement) {
121+
codeFragment.putCopyableUserData(KtCodeFragment.FAKE_CONTEXT_FOR_JAVA_FILE, {
122+
val emptyFile = createFakeFileWithJavaContextElement("", contextElement)
123+
124+
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
125+
val debuggerSession = debuggerContext.debuggerSession
126+
if ((debuggerSession == null || debuggerContext.suspendContext == null) && !ApplicationManager.getApplication().isUnitTestMode) {
127+
LOG.warn("Couldn't create fake context element for java file, debugger isn't paused on breakpoint")
128+
return@putCopyableUserData emptyFile
129+
}
130+
131+
// TODO: 'this' is unavailable
132+
val visibleVariables = getVisibleLocalVariables(contextElement, debuggerContext)
133+
if (visibleVariables == null) {
134+
LOG.warn("Couldn't get a list of local variables for ${debuggerContext.sourcePosition.file.name}:${debuggerContext.sourcePosition.line}")
135+
return@putCopyableUserData emptyFile
136+
}
137+
138+
val fakeFunctionText = StringBuilder().apply {
139+
append("fun _java_locals_debug_fun_() {\n")
140+
141+
val psiNameHelper = PsiNameHelper.getInstance(project)
142+
visibleVariables.forEach {
143+
val variable = it.key
144+
val variableName = variable.name()
145+
if (!psiNameHelper.isIdentifier(variableName)) return@forEach
146+
147+
val kotlinProperty = createKotlinProperty(project, variableName, variable.type().name(), it.value) ?: return@forEach
148+
append("$kotlinProperty\n")
149+
}
150+
151+
append("val _debug_context_val = 1\n")
152+
append("}")
153+
}.toString()
154+
155+
val fakeFile = createFakeFileWithJavaContextElement(fakeFunctionText, contextElement)
156+
val fakeFunction = fakeFile.declarations.firstOrNull() as? KtFunction
157+
val fakeContext = (fakeFunction?.bodyExpression as? KtBlockExpression)?.statements?.lastOrNull()
158+
159+
return@putCopyableUserData wrapContextIfNeeded(project, contextElement, fakeContext) ?: emptyFile
160+
})
161+
}
162+
120163
return codeFragment
121164
}
122165

166+
private fun getVisibleLocalVariables(contextElement: PsiElement?, debuggerContext: DebuggerContextImpl): Map<LocalVariable, Value>? {
167+
val semaphore = Semaphore()
168+
semaphore.down()
169+
170+
var visibleVariables: Map<LocalVariable, Value>? = null
171+
172+
val worker = object : DebuggerCommandImpl() {
173+
override fun action() {
174+
try {
175+
val frame = if (ApplicationManager.getApplication().isUnitTestMode)
176+
contextElement?.getCopyableUserData(DEBUG_CONTEXT_FOR_TESTS)?.frameProxy?.stackFrame
177+
else
178+
debuggerContext.frameProxy?.stackFrame
179+
180+
visibleVariables = frame?.let { it.getValues(it.visibleVariables()) } ?: emptyMap<LocalVariable, Value>()
181+
}
182+
catch(ignored: AbsentInformationException) {
183+
// Debug info unavailable
184+
}
185+
finally {
186+
semaphore.up()
187+
}
188+
}
189+
}
190+
191+
debuggerContext.debugProcess?.managerThread?.invoke(worker)
192+
193+
for (i in 0..50) {
194+
if (semaphore.waitFor(20)) break
195+
}
196+
197+
return visibleVariables
198+
}
199+
123200
private fun initImports(imports: String?): String? {
124201
if (imports != null && !imports.isEmpty()) {
125202
return imports.split(KtCodeFragment.IMPORT_SEPARATOR)
@@ -142,8 +219,11 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
142219
return import
143220
}
144221

145-
private fun getWrappedContextElement(project: Project, context: PsiElement?)
146-
= wrapContextIfNeeded(project, context, getContextElement(context))
222+
private fun getWrappedContextElement(project: Project, context: PsiElement?): PsiElement? {
223+
val newContext = getContextElement(context)
224+
if (newContext !is KtElement) return newContext
225+
return wrapContextIfNeeded(project, context, newContext)
226+
}
147227

148228
override fun createPresentationCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment {
149229
val kotlinCodeFragment = createCodeFragment(item, context, project)
@@ -190,11 +270,16 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
190270
}
191271

192272
override fun isContextAccepted(contextElement: PsiElement?): Boolean {
193-
if (contextElement is PsiCodeBlock) {
194-
// PsiCodeBlock -> DummyHolder -> originalElement
195-
return isContextAccepted(contextElement.context?.context)
273+
return when {
274+
// PsiCodeBlock -> DummyHolder -> originalElement
275+
contextElement is PsiCodeBlock -> isContextAccepted(contextElement.context?.context)
276+
contextElement == null -> false
277+
contextElement.language == KotlinFileType.INSTANCE.language -> true
278+
contextElement.language == JavaFileType.INSTANCE.language -> {
279+
getKotlinJvmRuntimeMarkerClass(contextElement.project, contextElement.resolveScope) != null
280+
}
281+
else -> false
196282
}
197-
return contextElement?.language == KotlinFileType.INSTANCE.language
198283
}
199284

200285
override fun getFileType() = KotlinFileType.INSTANCE
@@ -206,7 +291,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
206291
val DEBUG_LABEL_SUFFIX: String = "_DebugLabel"
207292
@TestOnly val DEBUG_CONTEXT_FOR_TESTS: Key<DebuggerContextImpl> = Key.create("DEBUG_CONTEXT_FOR_TESTS")
208293

209-
fun getContextElement(elementAt: PsiElement?): KtElement? {
294+
fun getContextElement(elementAt: PsiElement?): PsiElement? {
210295
if (elementAt == null) return null
211296

212297
if (elementAt is PsiCodeBlock) {
@@ -218,6 +303,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
218303
}
219304

220305
val containingFile = elementAt.containingFile
306+
if (containingFile is PsiJavaFile) return elementAt
221307
if (containingFile !is KtFile) return null
222308

223309
// 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() {
300386
return createWrappingContext(text, labels, newContext, project)
301387
}
302388

389+
private fun createFakeFileWithJavaContextElement(funWithLocalVariables: String, javaContext: PsiElement): KtFile {
390+
val javaFile = javaContext.containingFile as? PsiJavaFile
391+
392+
val sb = StringBuilder()
393+
394+
javaFile?.packageName?.check { !it.isBlank() }?.let {
395+
sb.append("package ").append(it.quoteIfNeeded()).append("\n")
396+
}
397+
398+
javaFile?.importList?.let { sb.append(it.text).append("\n") }
399+
400+
sb.append(funWithLocalVariables)
401+
402+
return KtPsiFactory(javaContext.project).createAnalyzableFile("fakeFileForJavaContextInDebugger.kt", sb.toString(), javaContext)
403+
}
404+
303405
// internal for test
304406
fun createWrappingContext(
305407
newFragmentText: String,
306408
labels: Map<String, Value>,
307-
originalContext: PsiElement?,
409+
originalContext: KtElement?,
308410
project: Project
309411
): KtElement? {
310412
val codeFragment = KtPsiFactory(project).createBlockCodeFragment(newFragmentText, originalContext)
@@ -318,6 +420,6 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
318420
}
319421
})
320422

321-
return getContextElement(codeFragment.findElementAt(codeFragment.text.length - 1))
423+
return getContextElement(codeFragment.findElementAt(codeFragment.text.length - 1)) as? KtElement
322424
}
323425
}

idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,18 @@ object KotlinEvaluationBuilder: EvaluatorBuilder {
9292
return EvaluatorBuilderImpl.getInstance()!!.build(codeFragment, position)
9393
}
9494

95-
val file = position.file
96-
if (file !is KtFile) {
97-
throw EvaluateExceptionUtil.createEvaluateException("Couldn't evaluate kotlin expression in non-kotlin context")
98-
}
99-
10095
if (position.line < 0) {
10196
throw EvaluateExceptionUtil.createEvaluateException("Couldn't evaluate kotlin expression at $position")
10297
}
10398

104-
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
105-
if (document == null || document.lineCount < position.line) {
106-
throw EvaluateExceptionUtil.createEvaluateException(
107-
"Couldn't evaluate kotlin expression: breakpoint is placed outside the file. " +
108-
"It may happen when you've changed source file after starting a debug process.")
99+
val file = position.file
100+
if (file is KtFile) {
101+
val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
102+
if (document == null || document.lineCount < position.line) {
103+
throw EvaluateExceptionUtil.createEvaluateException(
104+
"Couldn't evaluate kotlin expression: breakpoint is placed outside the file. " +
105+
"It may happen when you've changed source file after starting a debug process.")
106+
}
109107
}
110108

111109
if (codeFragment.context !is KtElement) {

idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/extractFunctionForDebuggerUtil.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ fun getFunctionForExtractedFragment(
8282
}
8383

8484
fun generateFunction(): ExtractionResult? {
85-
val originalFile = breakpointFile as KtFile
85+
val originalFile = codeFragment.getContextContainingFile() ?: return null
8686

8787
val newDebugExpressions = addDebugExpressionIntoTmpFileForExtractFunction(originalFile, codeFragment, breakpointLine)
8888
if (newDebugExpressions.isEmpty()) return null
@@ -124,6 +124,7 @@ fun addDebugExpressionIntoTmpFileForExtractFunction(originalFile: KtFile, codeFr
124124

125125
val tmpFile = originalFile.copy() as KtFile
126126
tmpFile.suppressDiagnosticsInDebugMode = true
127+
tmpFile.analysisContext = originalFile.analysisContext
127128

128129
val contextElement = getExpressionToAddDebugExpressionBefore(tmpFile, codeFragment.getOriginalContext(), line) ?: return emptyList()
129130

idea/testData/debugger/tinyApp/outs/allFilesPresentInJavaContext.out

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
LineBreakpoint created at jcBlock.kt:6
2+
!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
3+
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
4+
jcBlock.kt:6
5+
JavaClass.java:16
6+
JavaClass.java:18
7+
JavaClass.java:19
8+
Compile bytecode for bodyVal
9+
Compile bytecode for thenVal
10+
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
11+
12+
Process finished with exit code 0
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
LineBreakpoint created at jcImports.kt:6
2+
!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
3+
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
4+
jcImports.kt:6
5+
JavaClass.java:27
6+
JavaClass.java:28
7+
Compile bytecode for list.filter { it == 1 }.size
8+
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
9+
10+
Process finished with exit code 0
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
LineBreakpoint created at jcLocalVariable.kt:6
2+
!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
3+
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
4+
jcLocalVariable.kt:6
5+
JavaClass.java:11
6+
JavaClass.java:12
7+
Compile bytecode for i
8+
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
9+
10+
Process finished with exit code 0
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
LineBreakpoint created at jcMarkedObject.kt:6
2+
!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
3+
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
4+
jcMarkedObject.kt:6
5+
JavaClass.java:39
6+
JavaClass.java:40
7+
Compile bytecode for i
8+
Compile bytecode for i_DebugLabel
9+
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
10+
11+
Process finished with exit code 0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
LineBreakpoint created at jcSimple.kt:6
2+
!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
3+
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
4+
jcSimple.kt:6
5+
JavaClass.java:7
6+
Compile bytecode for 1 + 1
7+
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
8+
9+
Process finished with exit code 0
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package jcBlock
2+
3+
fun main(args: Array<String>) {
4+
val javaClass = forTests.javaContext.JavaClass()
5+
//Breakpoint!
6+
javaClass.block()
7+
}
8+
9+
// STEP_INTO: 1
10+
// STEP_OVER: 2
11+
12+
// EXPRESSION: bodyVal
13+
// RESULT: 1: I
14+
15+
// EXPRESSION: thenVal
16+
// RESULT: 1: I
17+
18+
// EXPRESSION: elseVal
19+
// RESULT: Unresolved reference: elseVal

0 commit comments

Comments
 (0)