Debugger: get correct context for breakpoints inside lambdas

This commit is contained in:
Natalia Ukhorskaya
2014-09-11 14:15:27 +04:00
parent a1e586cf7c
commit 50dcef254d
19 changed files with 351 additions and 38 deletions
@@ -83,18 +83,10 @@ public class CodeInsightUtils {
parent = parent.getParent();
}
if (!parent.equals(element1)) {
while (!parent.equals(element1.getParent())) {
element1 = element1.getParent();
}
}
element1 = getTopmostParentInside(element1, parent);
if (startOffset != element1.getTextRange().getStartOffset()) return PsiElement.EMPTY_ARRAY;
if (!parent.equals(element2)) {
while (!parent.equals(element2.getParent())) {
element2 = element2.getParent();
}
}
element2 = getTopmostParentInside(element2, parent);
if (endOffset != element2.getTextRange().getEndOffset()) return PsiElement.EMPTY_ARRAY;
List<PsiElement> array = new ArrayList<PsiElement>();
@@ -133,6 +125,47 @@ public class CodeInsightUtils {
return jetExpression;
}
@Nullable
public static PsiElement[] findElementsOfClassInRange(@NotNull PsiFile file, int startOffset, int endOffset, Class<? extends PsiElement> aClass) {
PsiElement element1 = getElementAtOffsetIgnoreWhitespaceBefore(file, startOffset);
PsiElement element2 = getElementAtOffsetIgnoreWhitespaceAfter(file, endOffset);
if (element1 == null || element2 == null) return PsiElement.EMPTY_ARRAY;
startOffset = element1.getTextRange().getStartOffset();
endOffset = element2.getTextRange().getEndOffset();
PsiElement parent = PsiTreeUtil.findCommonParent(element1, element2);
if (parent == null) return PsiElement.EMPTY_ARRAY;
element1 = getTopmostParentInside(element1, parent);
if (startOffset != element1.getTextRange().getStartOffset()) return PsiElement.EMPTY_ARRAY;
element2 = getTopmostParentInside(element2, parent);
if (endOffset != element2.getTextRange().getEndOffset()) return PsiElement.EMPTY_ARRAY;
PsiElement stopElement = element2.getNextSibling();
List<PsiElement> array = new ArrayList<PsiElement>();
for (PsiElement currentElement = element1; currentElement != stopElement; currentElement = currentElement.getNextSibling()) {
if (aClass.isInstance(currentElement)) {
array.add(currentElement);
}
array.addAll(PsiTreeUtil.findChildrenOfType(currentElement, aClass));
}
return PsiUtilCore.toPsiElementArray(array);
}
@NotNull
private static PsiElement getTopmostParentInside(@NotNull PsiElement element, @NotNull PsiElement parent) {
if (!parent.equals(element)) {
while (!parent.equals(element.getParent())) {
element = element.getParent();
}
}
return element;
}
@Nullable
public static PsiElement getElementAtOffsetIgnoreWhitespaceBefore(@NotNull PsiFile file, int offset) {
PsiElement element = file.findElementAt(offset);
@@ -206,6 +239,15 @@ public class CodeInsightUtils {
return CharArrayUtil.shiftForward(document.getCharsSequence(), lineStartOffset, " \t");
}
@Nullable
public static Integer getEndLineOffset(@NotNull PsiFile file, int line) {
Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) return null;
int lineStartOffset = document.getLineEndOffset(line);
return CharArrayUtil.shiftBackward(document.getCharsSequence(), lineStartOffset, " \t");
}
@Nullable
public static PsiElement getTopmostElementAtOffset(@NotNull PsiElement element, int offset) {
do {
@@ -56,6 +56,7 @@ import org.jetbrains.jet.lang.resolve.name.FqName;
import org.jetbrains.jet.lang.types.lang.InlineStrategy;
import org.jetbrains.jet.lang.types.lang.InlineUtil;
import org.jetbrains.jet.plugin.caches.resolve.ResolvePackage;
import org.jetbrains.jet.plugin.codeInsight.CodeInsightUtils;
import org.jetbrains.jet.plugin.project.ResolveSessionForBodies;
import org.jetbrains.jet.plugin.util.DebuggerUtils;
import org.jetbrains.org.objectweb.asm.Type;
@@ -93,12 +94,48 @@ public class JetPositionManager implements PositionManager {
}
if (lineNumber >= 0) {
JetFunctionLiteral lambdaIfInside = getLambdaIfInside(location, (JetFile) psiFile, lineNumber);
if (lambdaIfInside != null) {
return SourcePosition.createFromElement(lambdaIfInside.getBodyExpression().getStatements().get(0));
}
return SourcePosition.createFromLine(psiFile, lineNumber);
}
throw new NoDataException();
}
private JetFunctionLiteral getLambdaIfInside(@NotNull Location location, @NotNull JetFile file, int lineNumber) {
String currentLocationFqName = location.declaringType().name();
if (currentLocationFqName == null) return null;
Integer start = CodeInsightUtils.getStartLineOffset(file, lineNumber);
Integer end = CodeInsightUtils.getEndLineOffset(file, lineNumber);
if (start == null || end == null) return null;
PsiElement[] literals = CodeInsightUtils.findElementsOfClassInRange(file, start, end, JetFunctionLiteral.class);
if (literals == null || literals.length == 0) return null;
boolean isInLibrary = LibraryUtil.findLibraryEntry(file.getVirtualFile(), file.getProject()) != null;
JetTypeMapper typeMapper = !isInLibrary
? prepareTypeMapper(file)
: createTypeMapperForLibraryFile(file.findElementAt(start), file);
String currentLocationClassName = JvmClassName.byFqNameWithoutInnerClasses(new FqName(currentLocationFqName)).getInternalName();
for (PsiElement literal : literals) {
JetFunctionLiteral functionLiteral = (JetFunctionLiteral) literal;
if (isInlinedLambda(functionLiteral, typeMapper.getBindingContext())) {
continue;
}
String internalClassName = getClassNameForElement(literal.getFirstChild(), typeMapper, file, isInLibrary);
if (internalClassName.equals(currentLocationClassName)) {
return functionLiteral;
}
}
return null;
}
@Nullable
private PsiFile getPsiFileByLocation(@NotNull Location location) {
String sourceName;
@@ -142,7 +179,7 @@ public class JetPositionManager implements PositionManager {
public void run() {
JetFile file = (JetFile) sourcePosition.getFile();
boolean isInLibrary = LibraryUtil.findLibraryEntry(file.getVirtualFile(), file.getProject()) != null;
JetTypeMapper typeMapper = !isInLibrary ? prepareTypeMapper(file) : createTypeMapperForLibraryFile(sourcePosition);
JetTypeMapper typeMapper = !isInLibrary ? prepareTypeMapper(file) : createTypeMapperForLibraryFile(sourcePosition.getElementAt(), file);
result.set(getClassNameForElement(sourcePosition.getElementAt(), typeMapper, file, isInLibrary));
}
@@ -217,11 +254,10 @@ public class JetPositionManager implements PositionManager {
return typeMapper.mapClass(classDescriptor).getInternalName();
}
private static JetTypeMapper createTypeMapperForLibraryFile(@NotNull SourcePosition position) {
JetElement element = getElementToCreateTypeMapperForLibraryFile(position.getElementAt());
private static JetTypeMapper createTypeMapperForLibraryFile(@Nullable PsiElement notPositionedElement, @NotNull JetFile file) {
JetElement element = getElementToCreateTypeMapperForLibraryFile(notPositionedElement);
ResolveSessionForBodies resolveSession = ResolvePackage.getLazyResolveSession(element);
JetFile file = (JetFile) position.getFile();
GenerationState state = new GenerationState(file.getProject(), ClassBuilderFactories.THROW_EXCEPTION,
resolveSession.getModuleDescriptor(),
resolveSession.resolveToElement(element),
@@ -77,16 +77,11 @@ fun getFunctionForExtractedFragment(
val originalFile = breakpointFile as JetFile
val lineStart = CodeInsightUtils.getStartLineOffset(originalFile, breakpointLine)
if (lineStart == null) return null
val tmpFile = originalFile.createTempCopy { it }
tmpFile.skipVisibilityCheck = true
val elementAtOffset = tmpFile.findElementAt(lineStart)
if (elementAtOffset == null) return null
val contextElement: PsiElement = CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, lineStart) ?: elementAtOffset
val contextElement = getExpressionToAddDebugExpressionBefore(tmpFile, codeFragment.getContext(), breakpointLine)
if (contextElement == null) return null
// Don't evaluate smth when breakpoint is on package directive (ex. for package classes)
if (contextElement is JetFile) {
@@ -137,6 +132,52 @@ private fun addImportsToFile(newImportList: JetImportList?, tmpFile: JetFile) {
}
}
private fun JetFile.getElementInCopy(e: PsiElement): PsiElement? {
val offset = e.getTextRange()?.getStartOffset()
if (offset == null) {
return null
}
var elementAt = this.findElementAt(offset)
while (elementAt == null || elementAt!!.getTextRange()?.getEndOffset() != e.getTextRange()?.getEndOffset()) {
elementAt = elementAt?.getParent()
}
return elementAt
}
private fun getExpressionToAddDebugExpressionBefore(tmpFile: JetFile, contextElement: PsiElement?, line: Int): PsiElement? {
if (contextElement == null) {
val lineStart = CodeInsightUtils.getStartLineOffset(tmpFile, line)
if (lineStart == null) return null
val elementAtOffset = tmpFile.findElementAt(lineStart)
if (elementAtOffset == null) return null
return CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, lineStart) ?: elementAtOffset
}
fun shouldStop(el: PsiElement?, p: PsiElement?) = p is JetBlockExpression || el is JetDeclaration
var elementAt = tmpFile.getElementInCopy(contextElement)
var parent = elementAt?.getParent()
if (shouldStop(elementAt, parent)) {
return elementAt
}
var parentOfParent = parent?.getParent()
while (parent != null && parentOfParent != null) {
if (shouldStop(parent, parentOfParent)) {
break
}
parent = parent?.getParent()
parentOfParent = parent?.getParent()
}
return parent
}
private fun addDebugExpressionBeforeContextElement(codeFragment: JetCodeFragment, contextElement: PsiElement): JetExpression? {
val psiFactory = JetPsiFactory(codeFragment)
@@ -144,6 +185,10 @@ private fun addDebugExpressionBeforeContextElement(codeFragment: JetCodeFragment
contextElement is JetProperty && !contextElement.isLocal() -> {
wrapInRunFun(contextElement.getDelegateExpressionOrInitializer()!!)
}
contextElement is JetFunctionLiteral -> {
val block = contextElement.getBodyExpression()!!
block.getStatements().first ?: block.getLastChild()
}
contextElement is JetDeclarationWithBody && !contextElement.hasBlockBody()-> {
wrapInRunFun(contextElement.getBodyExpression()!!)
}
@@ -0,0 +1,9 @@
LineBreakpoint created at callableBug.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!;!CUSTOM_LIBRARY!;!RT_JAR! callableBug.CallableBugPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
callableBug.kt:7
Compile bytecode for callable
callableBug.kt:7
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,8 @@
LineBreakpoint created at inlineLambda.kt:9
!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!;!CUSTOM_LIBRARY!;!RT_JAR! inlineLambda.InlineLambdaPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
inlineLambda.kt:8
inlineLambda.kt:8
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,10 @@
LineBreakpoint created at lambdaOnSecondLine.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!;!CUSTOM_LIBRARY!;!RT_JAR! lambdaOnSecondLine.LambdaOnSecondLinePackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
lambdaOnSecondLine.kt:9
lambdaOnSecondLine.kt:14
lambdaOnSecondLine.kt:9
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,10 @@
LineBreakpoint created at oneLineLambda.kt:9
!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!;!CUSTOM_LIBRARY!;!RT_JAR! oneLineLambda.OneLineLambdaPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
oneLineLambda.kt:8
oneLineLambda.kt:13
oneLineLambda.kt:8
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,10 @@
LineBreakpoint created at twoLambdasOnOneLineFirst.kt:9
!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!;!CUSTOM_LIBRARY!;!RT_JAR! twoLambdasOnOneLineFirst.TwoLambdasOnOneLineFirstPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
twoLambdasOnOneLineFirst.kt:8
twoLambdasOnOneLineFirst.kt:13
twoLambdasOnOneLineFirst.kt:8
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,12 @@
LineBreakpoint created at twoLambdasOnOneLineSecond.kt:9
!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!;!CUSTOM_LIBRARY!;!RT_JAR! twoLambdasOnOneLineSecond.TwoLambdasOnOneLineSecondPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
twoLambdasOnOneLineSecond.kt:8
twoLambdasOnOneLineSecond.kt:15
twoLambdasOnOneLineSecond.kt:8
twoLambdasOnOneLineSecond.kt:15
twoLambdasOnOneLineSecond.kt:8
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,12 @@
package callableBug
fun main(args: Array<String>) {
val callable = 1
array(1, 2).map {
it + 1
//Breakpoint!
}.forEach { it + 2 }
}
// EXPRESSION: callable
// RESULT: 1: I
@@ -0,0 +1,11 @@
package inlineLambda
fun main(args: Array<String>) {
val a = array(1)
// EXPRESSION: it
// RESULT: Unresolved reference: it
// STEP_INTO: 1
//Breakpoint!
a.map { it * 1 }
}
@@ -0,0 +1,18 @@
package lambdaOnSecondLine
fun main(args: Array<String>) {
val a = A()
// EXPRESSION: it
// RESULT: 1: I
// STEP_INTO: 2
a.foo { a }
//Breakpoint!
.foo { a }
}
class A {
fun foo(f: (Int) -> A): A {
return f(1)
}
}
@@ -0,0 +1,16 @@
package oneLineLambda
fun main(args: Array<String>) {
val a = A()
// EXPRESSION: it
// RESULT: 1: I
// STEP_INTO: 2
//Breakpoint!
a.foo { a }
}
class A {
fun foo(f: (Int) -> A): A {
return f(1)
}
}
@@ -0,0 +1,16 @@
package twoLambdasOnOneLineFirst
fun main(args: Array<String>) {
val a = A()
// EXPRESSION: it
// RESULT: 1: I
// STEP_INTO: 2
//Breakpoint!
a.foo { a }.foo { a }
}
class A {
fun foo(f: (Int) -> A): A {
return f(1)
}
}
@@ -0,0 +1,18 @@
package twoLambdasOnOneLineSecond
fun main(args: Array<String>) {
val a = A()
// EXPRESSION: it
// RESULT: 2: I
// STEP_INTO: 4
//Breakpoint!
a.foo { counter++; a }.foo { a }
}
var counter = 1
class A {
fun foo(f: (Int) -> A): A {
return f(counter)
}
}
@@ -59,7 +59,7 @@ import com.intellij.debugger.DebuggerManagerEx
import com.intellij.psi.PsiDocumentManager
import com.intellij.openapi.application.ModalityState
public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestCase() {
public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
private val logger = Logger.getLogger(javaClass<KotlinEvaluateExpressionCache>())!!
private val appender = object : AppenderSkeleton() {
@@ -73,7 +73,7 @@ public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestC
private var oldLogLevel: Level? = null
override fun setUp() {
super<KotlinDebuggerTestCase>.setUp()
super.setUp()
oldLogLevel = logger.getLevel()
logger.setLevel(Level.DEBUG)
@@ -84,7 +84,7 @@ public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestC
logger.setLevel(oldLogLevel)
logger.removeAppender(appender)
super<KotlinDebuggerTestCase>.tearDown()
super.tearDown()
}
fun doSingleBreakpointTest(path: String) {
@@ -102,6 +102,13 @@ public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestC
createDebugProcess(path)
val count = InTextDirectivesUtils.getPrefixedInt(fileText, "// STEP_INTO: ") ?: 0
if (count > 0) {
for (i in 1..count) {
onBreakpoint { stepInto() }
}
}
onBreakpoint {
val exceptions = linkedMapOf<String, Throwable>()
try {
@@ -275,19 +282,6 @@ public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestC
return mainFile.getParentFile()?.listFiles()?.filter { it.name.startsWith(mainFileName) && it.name != mainFileName } ?: Collections.emptyList()
}
private fun onBreakpoint(doOnBreakpoint: SuspendContextImpl.() -> Unit) {
super.onBreakpoint {
super.printContext(it)
it.doOnBreakpoint()
}
}
private fun finish() {
onBreakpoint {
resume(this)
}
}
private fun SuspendContextImpl.evaluate(text: String, codeFragmentKind: CodeFragmentKind, expectedResult: String) {
ApplicationManager.getApplication()?.runReadAction {
val sourcePosition = ContextUtil.getSourcePosition(this)
@@ -35,7 +35,7 @@ import java.util.regex.Pattern;
public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluateExpressionTest {
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint")
@TestDataPath("$PROJECT_ROOT")
@InnerTestClasses({SingleBreakpoint.Frame.class})
@InnerTestClasses({SingleBreakpoint.Frame.class, SingleBreakpoint.Lambdas.class})
@RunWith(org.jetbrains.jet.JUnit3RunnerWithInners.class)
public static class SingleBreakpoint extends AbstractKotlinEvaluateExpressionTest {
@TestMetadata("abstractFunCall.kt")
@@ -54,6 +54,12 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
doSingleBreakpointTest(fileName);
}
@TestMetadata("callableBug.kt")
public void testCallableBug() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/callableBug.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("classFromAnotherPackage.kt")
public void testClassFromAnotherPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/classFromAnotherPackage.kt");
@@ -262,6 +268,46 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
}
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas")
@TestDataPath("$PROJECT_ROOT")
@RunWith(org.jetbrains.jet.JUnit3RunnerWithInners.class)
public static class Lambdas extends AbstractKotlinEvaluateExpressionTest {
public void testAllFilesPresentInLambdas() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("inlineLambda.kt")
public void testInlineLambda() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas/inlineLambda.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("lambdaOnSecondLine.kt")
public void testLambdaOnSecondLine() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas/lambdaOnSecondLine.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("oneLineLambda.kt")
public void testOneLineLambda() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas/oneLineLambda.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("twoLambdasOnOneLineFirst.kt")
public void testTwoLambdasOnOneLineFirst() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas/twoLambdasOnOneLineFirst.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("twoLambdasOnOneLineSecond.kt")
public void testTwoLambdasOnOneLineSecond() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas/twoLambdasOnOneLineSecond.kt");
doSingleBreakpointTest(fileName);
}
}
}
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/multipleBreakpoints")