Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Debugger: use extract method to get function arguments
  • Loading branch information
NataliaUkhorskaya committed Apr 23, 2014
1 parent 084c72f commit 2ffcc51
Show file tree
Hide file tree
Showing 25 changed files with 422 additions and 64 deletions.
5 changes: 5 additions & 0 deletions annotations/com/intellij/debugger/engine/jdi/annotations.xml
@@ -0,0 +1,5 @@
<root>
<item name='com.intellij.debugger.engine.jdi.StackFrameProxy com.sun.jdi.StackFrame getStackFrame()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -1,15 +1,14 @@
package org.jetbrains.jet.plugin.codeInsight;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.descriptors.FunctionDescriptor;
Expand Down Expand Up @@ -188,4 +187,27 @@ public static String createFunctionSignatureStringFromDescriptor(
DescriptorRenderer renderer = shortTypeNames ? DescriptorRenderer.SOURCE_CODE_SHORT_NAMES_IN_TYPES : DescriptorRenderer.SOURCE_CODE;
return renderer.render(descriptor);
}

@Nullable
public static Integer getStartLineOffset(@NotNull PsiFile file, int line) {
Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) return null;

int lineStartOffset = document.getLineStartOffset(line);
return CharArrayUtil.shiftForward(document.getCharsSequence(), lineStartOffset, " \t");
}

@Nullable
public static PsiElement getTopmostElementAtOffset(@NotNull PsiElement element, int offset) {
do {
PsiElement parent = element.getParent();
if (parent == null || (parent.getTextOffset() < offset)) {
break;
}
element = parent;
}
while(true);

return element;
}
}
Expand Up @@ -40,6 +40,8 @@ import com.sun.jdi.Location
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiFile
import com.intellij.openapi.editor.Editor
import org.jetbrains.jet.plugin.codeInsight.CodeInsightUtils
import com.intellij.psi.PsiDocumentManager

public class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() {

Expand All @@ -49,27 +51,22 @@ public class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() {
if (position.getLine() < 0) return Collections.emptyList()

val file = position.getFile()
val vFile = file.getVirtualFile()
if (vFile == null) return Collections.emptyList()

val doc = FileDocumentManager.getInstance()?.getDocument(vFile)
if (doc == null) return Collections.emptyList()

val line = position.getLine()
if (line >= doc.getLineCount()) return Collections.emptyList()
val lineStart = CodeInsightUtils.getStartLineOffset(file, position.getLine())
if (lineStart == null) return Collections.emptyList()

val lineStartOffset = doc.getLineStartOffset(line)
val offsetWithoutTab = CharArrayUtil.shiftForward(doc.getCharsSequence(), lineStartOffset, " \t")
val elementAtOffset = file.findElementAt(offsetWithoutTab)
val elementAtOffset = file.findElementAt(lineStart)
if (elementAtOffset == null) return Collections.emptyList()

val element = getTopmostElementAtOffset(elementAtOffset, lineStartOffset)

val element = CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, lineStart)
if (element !is JetElement) return Collections.emptyList()

val elementTextRange = element.getTextRange()
if (elementTextRange == null) return Collections.emptyList()

val doc = PsiDocumentManager.getInstance(file.getProject()).getDocument(file)
if (doc == null) return Collections.emptyList()

val lines = Range<Int>(doc.getLineNumber(elementTextRange.getStartOffset()), doc.getLineNumber(elementTextRange.getEndOffset()))
val bindingContext = AnalyzerFacadeWithCache.getContextForElement(element)
val result = OrderedSet<SmartStepTarget>()
Expand Down
Expand Up @@ -45,18 +45,35 @@ import org.jetbrains.jet.lang.resolve.java.PackageClassUtils
import org.jetbrains.jet.lang.resolve.name.FqName
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import org.jetbrains.jet.plugin.debugger.KotlinEditorTextProvider
import org.jetbrains.jet.lang.psi.JetPsiFactory
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.jet.plugin.refactoring.createTempCopy
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionData
import org.jetbrains.jet.plugin.refactoring.extractFunction.performAnalysis
import org.jetbrains.jet.plugin.util.MaybeError
import org.jetbrains.jet.plugin.util.MaybeValue
import org.jetbrains.jet.plugin.refactoring.extractFunction.validate
import org.jetbrains.jet.plugin.refactoring.checkConflictsInteractively
import org.jetbrains.jet.plugin.refactoring.extractFunction.generateFunction
import org.jetbrains.jet.lang.psi.JetElement
import com.intellij.util.text.CharArrayUtil
import org.jetbrains.jet.lang.psi.JetNamedFunction
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import org.jetbrains.jet.plugin.codeInsight.CodeInsightUtils
import org.jetbrains.jet.OutputFileCollection
import org.jetbrains.jet.lang.psi.JetExpressionCodeFragment
import org.jetbrains.jet.lang.psi.JetExpressionCodeFragmentImpl
import org.jetbrains.jet.plugin.caches.resolve.getAnalysisResults

object KotlinEvaluationBuilder: EvaluatorBuilder {
override fun build(codeFragment: PsiElement, position: SourcePosition?): ExpressionEvaluator {
if (codeFragment !is JetExpressionCodeFragment) {
if (codeFragment !is JetExpressionCodeFragment || position == null) {
return EvaluatorBuilderImpl.getInstance()!!.build(codeFragment, position)
}

val elementAt = position?.getElementAt()
val elementAt = position.getElementAt()
if (elementAt != null) {
codeFragment.addImportsFromString(KotlinEditorTextProvider.getImports(elementAt))

Expand All @@ -65,19 +82,26 @@ object KotlinEvaluationBuilder: EvaluatorBuilder {
codeFragment.addImportsFromString("import $packageName.*")
}
}
return ExpressionEvaluatorImpl(KotlinEvaluator(codeFragment))
return ExpressionEvaluatorImpl(KotlinEvaluator(codeFragment, position))
}
}

class KotlinEvaluator(val codeFragment: PsiElement) : Evaluator {
class KotlinEvaluator(val codeFragment: PsiElement,
val sourcePosition: SourcePosition
) : Evaluator {
override fun evaluate(context: EvaluationContextImpl): Any? {
return ApplicationManager.getApplication()?.runReadAction(object: Computable<Any> {
override fun compute(): Any? {
if (codeFragment !is JetExpressionCodeFragment) {
throw AssertionError("KotlinEvaluator should be invoke only for KotlinCodeFragment")
}

val file = createFileForDebugger(codeFragment)
val extractedFunction = getFunctionForExtractedFragment(codeFragment, sourcePosition.getFile(), sourcePosition.getLine())
if (extractedFunction == null) {
exception("This code fragment cannot be extracted to function")
}

val file = createFileForDebugger(codeFragment, extractedFunction)

val analyzeExhaust = file.getAnalysisResults()
val bindingContext = analyzeExhaust.getBindingContext()
Expand Down Expand Up @@ -111,12 +135,12 @@ class KotlinEvaluator(val codeFragment: PsiElement) : Evaluator {

ClassReader(outputFiles.first().asByteArray()).accept(object : ClassVisitor(ASM5) {
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (name == "debugFun") {
if (name == extractedFunction.getName()) {
return object : MethodNode(Opcodes.ASM5, access, name, desc, signature, exceptions) {
override fun visitEnd() {
val value = interpreterLoop(
this,
makeInitialFrame(this, listOf()),
makeInitialFrame(this, context.getArgumentsByNames(extractedFunction.getParameterNamesForDebugger())),
JDIEval(virtualMachine,
context.getClassLoader()!!,
context.getSuspendContext().getThread()?.getThreadReference()!!)
Expand Down Expand Up @@ -146,6 +170,39 @@ class KotlinEvaluator(val codeFragment: PsiElement) : Evaluator {
return null
}

private fun JetNamedFunction.getParameterNamesForDebugger(): List<String> {
val result = arrayListOf<String>()
for (param in getValueParameters()) {
result.add(param.getName()!!)
}
if (getReceiverTypeRef() != null) {
result.add("this")
}
return result
}

private fun EvaluationContextImpl.getArgumentsByNames(parameterNames: List<String>): List<Value> {
val frames = getFrameProxy()?.getStackFrame()
if (frames != null) {
try {
return parameterNames.map {
name ->
if (name == "this") {
frames.thisObject().asValue()
}
else {
frames.getValue(frames.visibleVariableByName(name)).asValue()
}
}
}
catch(e: Throwable) {
throw IllegalArgumentException(
"Cannot get parameter values from VirtualMachine: ${e.javaClass}\nFunction parameters:\n${parameterNames.makeString("\n")}\nVisible variables:\n${frames.visibleVariables().makeString("\n")}")
}
}
return Collections.emptyList()
}

private fun exception(msg: String) = throw EvaluateExceptionUtil.createEvaluateException(msg)
}

Expand All @@ -154,22 +211,69 @@ package packageForDebugger
!IMPORT_LIST!
fun debugFun() = run {
!EXPRESSION!
}
!FUNCTION!
"""

private val packageInternalName = PackageClassUtils.getPackageClassFqName(FqName("packageForDebugger")).asString().replace(".", "/")

private fun createFileForDebugger(codeFragment: JetExpressionCodeFragment): JetFile {
var fileText = template.replace("!EXPRESSION!", codeFragment.getText())
fileText = fileText.replace("!IMPORT_LIST!",
codeFragment.importsToString()
.split(JetExpressionCodeFragmentImpl.IMPORT_SEPARATOR)
.makeString("\n"))
private fun createFileForDebugger(codeFragment: JetExpressionCodeFragment,
extractedFunction: JetNamedFunction
): JetFile {
var fileText = template.replace("!IMPORT_LIST!",
codeFragment.importsToString()
.split(JetExpressionCodeFragmentImpl.IMPORT_SEPARATOR)
.makeString("\n"))

fileText = fileText.replace("!FUNCTION!", extractedFunction.getText())

val virtualFile = LightVirtualFile("debugFile.kt", JetLanguage.INSTANCE, fileText)
virtualFile.setCharset(CharsetToolkit.UTF8_CHARSET)
return (PsiFileFactory.getInstance(codeFragment.getProject()) as PsiFileFactoryImpl)
.trySetupPsiForFile(virtualFile, JetLanguage.INSTANCE, true, false) as JetFile
.trySetupPsiForFile(virtualFile, JetLanguage.INSTANCE, true, false) as JetFile
}

private fun getFunctionForExtractedFragment(
codeFragment: PsiElement,
breakpointFile: PsiFile,
breakpointLine: Int
): JetNamedFunction? {
val project = codeFragment.getProject()

val originalFile = breakpointFile as JetFile

val lineStart = CodeInsightUtils.getStartLineOffset(originalFile, breakpointLine)
if (lineStart == null) return null

val tmpFile = originalFile.createTempCopy { it }
val elementAtOffset = tmpFile.findElementAt(lineStart)
if (elementAtOffset == null) return null

val element: PsiElement = CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, lineStart) ?: elementAtOffset

val debugExpression = JetPsiFactory.createExpression(project, codeFragment.getText())

val parent = element.getParent()
if (parent == null) return null

parent.addBefore(JetPsiFactory.createNewLine(project), element)
val newDebugExpression = parent.addBefore(debugExpression, element)
if (newDebugExpression == null) return null

parent.addBefore(JetPsiFactory.createNewLine(project), element)

val nextSibling = tmpFile.getDeclarations().firstOrNull()
if (nextSibling == null) return null

val analysisResult = ExtractionData(tmpFile, Collections.singletonList(newDebugExpression), nextSibling).performAnalysis()
if (analysisResult is MaybeError) {
throw EvaluateExceptionUtil.createEvaluateException(analysisResult.error)
}

val validationResult = (analysisResult as MaybeValue).value.validate()
if (!validationResult.conflicts.isEmpty()) {
throw EvaluateExceptionUtil.createEvaluateException("Some declarations are unavailable")
}

return validationResult.descriptor.generateFunction(true)
}

7 changes: 7 additions & 0 deletions idea/testData/debugger/tinyApp/outs/classObjectVal.out
@@ -0,0 +1,7 @@
LineBreakpoint created at classObjectVal.kt:10
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! classObjectVal.ClassObjectValPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
classObjectVal.kt:9
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
7 changes: 7 additions & 0 deletions idea/testData/debugger/tinyApp/outs/enums.out
@@ -0,0 +1,7 @@
LineBreakpoint created at enums.kt:7
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! enums.EnumsPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
enums.kt:6
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
7 changes: 7 additions & 0 deletions idea/testData/debugger/tinyApp/outs/extractLocalVariables.out
@@ -0,0 +1,7 @@
LineBreakpoint created at extractLocalVariables.kt:7
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! extractLocalVariables.ExtractLocalVariablesPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
extractLocalVariables.kt:6
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
7 changes: 7 additions & 0 deletions idea/testData/debugger/tinyApp/outs/extractThis.out
@@ -0,0 +1,7 @@
LineBreakpoint created at extractThis.kt:12
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! extractThis.ExtractThisPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
extractThis.kt:11
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
@@ -0,0 +1,7 @@
LineBreakpoint created at extractVariablesFromCall.kt:8
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! extractVariablesFromCall.ExtractVariablesFromCallPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
extractVariablesFromCall.kt:7
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
@@ -0,0 +1,7 @@
LineBreakpoint created at multilineExpressionAtBreakpoint.kt:5
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! multilineExpressionAtBreakpoint.MultilineExpressionAtBreakpointPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
multilineExpressionAtBreakpoint.kt:4
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
7 changes: 7 additions & 0 deletions idea/testData/debugger/tinyApp/outs/vars.out
@@ -0,0 +1,7 @@
LineBreakpoint created at vars.kt:7
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!RT_JAR! vars.VarsPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
vars.kt:6
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

Process finished with exit code 0
8 changes: 4 additions & 4 deletions idea/testData/debugger/tinyApp/src/evaluate/arrays.kt
Expand Up @@ -6,10 +6,10 @@ fun main(args: Array<String>) {
}

// EXPRESSION: array(1, 2).map { it.toString() }
// RESULT: instance of java.util.ArrayList(id=347): Ljava/util/ArrayList;
// RESULT: instance of java.util.ArrayList(id=ID): Ljava/util/ArrayList;

// EXPRESSION: array(1, 2, 101, 102).filter { it > 100 }
// RESULT: instance of java.util.ArrayList(id=369): Ljava/util/ArrayList;
// RESULT: instance of java.util.ArrayList(id=ID): Ljava/util/ArrayList;

// EXPRESSION: array(1, 2).none()
// RESULT: 0: Z
Expand All @@ -27,7 +27,7 @@ fun main(args: Array<String>) {
// RESULT: 2: I

// EXPRESSION: intArray(1, 2).max()
// RESULT: instance of java.lang.Integer(id=343): Ljava/lang/Integer;
// RESULT: instance of java.lang.Integer(id=ID): Ljava/lang/Integer;

// EXPRESSION: array(1, 2).max()
// RESULT: instance of java.lang.Integer(id=343): Ljava/lang/Integer;
// RESULT: instance of java.lang.Integer(id=ID): Ljava/lang/Integer;

0 comments on commit 2ffcc51

Please sign in to comment.