diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt index 3b80dc2bf35..20d4040c6f8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt @@ -18,9 +18,13 @@ package org.jetbrains.kotlin.idea.debugger.filter import com.intellij.debugger.engine.SyntheticTypeComponentProvider import com.sun.jdi.AbsentInformationException +import com.sun.jdi.ClassType import com.sun.jdi.Method import com.sun.jdi.TypeComponent +import org.jetbrains.kotlin.load.java.JvmAbi import org.jetbrains.kotlin.name.FqNameUnsafe +import org.jetbrains.org.objectweb.asm.Opcodes +import sun.tools.java.RuntimeConstants public class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvider { override fun isSynthetic(typeComponent: TypeComponent?): Boolean { @@ -30,6 +34,8 @@ public class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvide if (!FqNameUnsafe.isValid(typeName)) return false try { + if (typeComponent.isDelegateToDefaultInterfaceImpl()) return true + if (typeComponent.location().lineNumber() != 1) return false if (typeComponent.allLineLocations().any { it.lineNumber() != 1 }) { @@ -42,4 +48,60 @@ public class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvide return false } } + + private fun Method.isDelegateToDefaultInterfaceImpl(): Boolean { + if (allLineLocations().size != 1) return false + + if (!hasOnlyInvokeStatic(this)) return false + + return hasInterfaceWithImplementation(this) + } + + private val LOAD_INSTRUCTIONS_WITH_INDEX = Opcodes.ILOAD.toByte()..Opcodes.ALOAD.toByte() + private val LOAD_INSTRUCTIONS = (Opcodes.ALOAD + 1).toByte()..(Opcodes.IALOAD - 1).toByte() + + private val RETURN_INSTRUCTIONS = Opcodes.IRETURN.toByte()..Opcodes.RETURN.toByte() + + // Check that method contains only load and invokeStatic instructions. Note that if after load goes ldc instruction it could be checkParametersNotNull method invocation + private fun hasOnlyInvokeStatic(m: Method): Boolean { + val bytecodes = m.bytecodes() + var i = 0 + var isALoad0BeforeStaticCall = false + while (i < bytecodes.size) { + val instr = bytecodes[i] + when { + instr == 42.toByte() /* ALOAD_0 */ -> { + i += 1 + isALoad0BeforeStaticCall = true + } + instr in LOAD_INSTRUCTIONS_WITH_INDEX || instr in LOAD_INSTRUCTIONS -> { + i += 1 + if (instr in LOAD_INSTRUCTIONS_WITH_INDEX) i += 1 + val nextInstr = bytecodes[i] + if (nextInstr == Opcodes.LDC.toByte()) { + i += 2 + isALoad0BeforeStaticCall = false + } + } + instr == Opcodes.INVOKESTATIC.toByte() -> { + i += 3 + if (isALoad0BeforeStaticCall && i == (bytecodes.size - 1)) { + val nextInstr = bytecodes[i] + return nextInstr in RETURN_INSTRUCTIONS + } + } + else -> return false + } + } + return false + } + + // TODO: class DefaultImpl can be not loaded + private fun hasInterfaceWithImplementation(method: Method): Boolean { + val declaringType = method.declaringType() as? ClassType ?: return false + val interfaces = declaringType.allInterfaces() + val vm = declaringType.virtualMachine() + val traitImpls = interfaces.flatMap { vm.classesByName(it.name() + JvmAbi.DEFAULT_IMPLS_SUFFIX) } + return traitImpls.any { !it.methodsByName(method.name()).isEmpty() } + } } \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/outs/smartStepIntoInterfaceImpl.out b/idea/testData/debugger/tinyApp/outs/smartStepIntoInterfaceImpl.out new file mode 100644 index 00000000000..db60fc3c231 --- /dev/null +++ b/idea/testData/debugger/tinyApp/outs/smartStepIntoInterfaceImpl.out @@ -0,0 +1,40 @@ +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:90 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:95 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:100 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:105 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:110 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:120 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:125 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:130 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:138 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:143 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:148 +LineBreakpoint created at smartStepIntoInterfaceImpl.kt:154 +!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! smartStepIntoInterfaceImpl.SmartStepIntoInterfaceImplKt +Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' +smartStepIntoInterfaceImpl.kt:90 +smartStepIntoInterfaceImpl.kt:95 +smartStepIntoInterfaceImpl.kt:8 +smartStepIntoInterfaceImpl.kt:100 +smartStepIntoInterfaceImpl.kt:13 +smartStepIntoInterfaceImpl.kt:105 +smartStepIntoInterfaceImpl.kt:13 +smartStepIntoInterfaceImpl.kt:110 +smartStepIntoInterfaceImpl.kt:18 +smartStepIntoInterfaceImpl.kt:120 +smartStepIntoInterfaceImpl.kt:22 +smartStepIntoInterfaceImpl.kt:125 +smartStepIntoInterfaceImpl.kt:36 +smartStepIntoInterfaceImpl.kt:130 +smartStepIntoInterfaceImpl.kt:41 +smartStepIntoInterfaceImpl.kt:138 +MyJavaClass.java:16 +smartStepIntoInterfaceImpl.kt:143 +smartStepIntoInterfaceImpl.kt:59 +smartStepIntoInterfaceImpl.kt:148 +smartStepIntoInterfaceImpl.kt:64 +smartStepIntoInterfaceImpl.kt:154 +smartStepIntoInterfaceImpl.kt:70 +Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' + +Process finished with exit code 0 diff --git a/idea/testData/debugger/tinyApp/outs/syntheticProvider.out b/idea/testData/debugger/tinyApp/outs/syntheticProvider.out new file mode 100644 index 00000000000..8715915a630 --- /dev/null +++ b/idea/testData/debugger/tinyApp/outs/syntheticProvider.out @@ -0,0 +1,23 @@ +LineBreakpoint created at syntheticProvider.kt:38 +LineBreakpoint created at syntheticProvider.kt:42 +LineBreakpoint created at syntheticProvider.kt:46 +LineBreakpoint created at syntheticProvider.kt:50 +LineBreakpoint created at syntheticProvider.kt:54 +LineBreakpoint created at syntheticProvider.kt:58 +!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! syntheticProvider.SyntheticProviderKt +Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' +syntheticProvider.kt:38 +syntheticProvider.kt:5 +syntheticProvider.kt:42 +syntheticProvider.kt:9 +syntheticProvider.kt:46 +syntheticProvider.kt:13 +syntheticProvider.kt:50 +syntheticProvider.kt:17 +syntheticProvider.kt:54 +syntheticProvider.kt:21 +syntheticProvider.kt:58 +syntheticProvider.kt:25 +Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' + +Process finished with exit code 0 diff --git a/idea/testData/debugger/tinyApp/outs/traits.out b/idea/testData/debugger/tinyApp/outs/traits.out index 6cc5756cc2a..f4d966c57df 100644 --- a/idea/testData/debugger/tinyApp/outs/traits.out +++ b/idea/testData/debugger/tinyApp/outs/traits.out @@ -5,12 +5,9 @@ traits.kt:5 traits.kt:6 traits.kt:41 traits.kt:27 -traits.kt:41 traits.kt:6 traits.kt:7 -traits.kt:41 traits.kt:31 -traits.kt:41 traits.kt:7 traits.kt:8 traits.kt:43 @@ -20,24 +17,16 @@ traits.kt:47 traits.kt:9 traits.kt:11 traits.kt:19 -traits.kt:11 traits.kt:27 -traits.kt:11 traits.kt:19 traits.kt:20 -traits.kt:11 traits.kt:31 -traits.kt:11 traits.kt:20 traits.kt:21 traits.kt:13 -traits.kt:34 -traits.kt:13 traits.kt:21 traits.kt:22 traits.kt:17 -traits.kt:38 -traits.kt:17 traits.kt:22 traits.kt:23 Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' diff --git a/idea/testData/debugger/tinyApp/src/forTests/MyJavaClass.java b/idea/testData/debugger/tinyApp/src/forTests/MyJavaClass.java index 04745a60574..7d95c88844e 100644 --- a/idea/testData/debugger/tinyApp/src/forTests/MyJavaClass.java +++ b/idea/testData/debugger/tinyApp/src/forTests/MyJavaClass.java @@ -11,4 +11,8 @@ public class MyJavaClass { public String testNotNullFun() { return "a"; } + + public static int staticFun(Object s) { + return 1; + } } diff --git a/idea/testData/debugger/tinyApp/src/stepping/custom/smartStepIntoInterfaceImpl.kt b/idea/testData/debugger/tinyApp/src/stepping/custom/smartStepIntoInterfaceImpl.kt new file mode 100644 index 00000000000..ecb5e562210 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/stepping/custom/smartStepIntoInterfaceImpl.kt @@ -0,0 +1,155 @@ +package smartStepIntoInterfaceImpl + +import forTests.MyJavaClass + +interface I { + // without params + fun foo() { + val a = 1 + } + + // default param + fun foo(a: Int, b: Int = 1) { + val a = 1 + } + + // checkParamNotNull + fun foo(a: String) { + val a = 1 + } + + fun fooOverride() { + val a = 1 + } + + fun staticCallInOverride(s: String) = 1 +} + +class IImpl: I { + // super call in override (step into doesn't work) + override fun fooOverride() { + return super.fooOverride() + } + + // java static call + fun staticCall(s: String): Int { + return MyJavaClass.staticFun(s) + } + + // java static call in override + override fun staticCallInOverride(s: String): Int { + return MyJavaClass.staticFun(s) + } +} + +class IImpl2: I { + // java static call in override with this param (step into doesn't work) + override fun staticCallInOverride(s: String): Int { + return MyJavaClass.staticFun(this) + } +} + +object Obj: I { + @JvmStatic private fun staticFun(s: String): Int { + return 1 + } + + // kotlin static call + fun staticCall(s: String): Int { + return staticFun(s) + } + + // kotlin static call in override + override fun staticCallInOverride(s: String): Int { + return staticFun(s) + } +} + +object Obj2: I { + @JvmStatic private fun staticFun(s: Obj2): Int { + return 1 + } + + // kotlin static call in override with this param (step into doesn't work) + override fun staticCallInOverride(s: String): Int { + return staticFun(this) + } +} + +fun barI() = IImpl() + +fun main(args: Array) { + testSmartStepInto() + testStepInto() +} + +fun testSmartStepInto() { + // TODO First time - doesn't work: should be smartStepIntoInterfaceImpl.kt:8 between smartStepIntoInterfaceImpl.kt:90 and 95 + // SMART_STEP_INTO_BY_INDEX: 2 + //Breakpoint! + barI().foo() + + // SMART_STEP_INTO_BY_INDEX: 2 + // RESUME: 1 + //Breakpoint! + barI().foo() + + // SMART_STEP_INTO_BY_INDEX: 2 + // RESUME: 1 + //Breakpoint! + barI().foo(1) + + // SMART_STEP_INTO_BY_INDEX: 2 + // RESUME: 1 + //Breakpoint! + barI().foo(1, 2) + + // SMART_STEP_INTO_BY_INDEX: 2 + // RESUME: 1 + //Breakpoint! + barI().foo("a") +} + +fun testStepInto() { + val ii = IImpl() + + // TODO: should be smartStepIntoInterfaceImpl.kt:31 instead of smartStepIntoInterfaceImpl.kt:22 + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + ii.fooOverride() + + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + ii.staticCall("a") + + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + ii.staticCallInOverride("a") + + val ii2 = IImpl2() + + // TODO: should be smartStepIntoInterfaceImpl.kt:48 instead of MyJavaClass.java:16 + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + ii2.staticCallInOverride("a") + + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + Obj.staticCall("a") + + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + Obj.staticCallInOverride("a") + + // TODO: should be smartStepIntoInterfaceImpl.kt:75 instead of smartStepIntoInterfaceImpl.kt:70 + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + Obj2.staticCallInOverride("a") +} diff --git a/idea/testData/debugger/tinyApp/src/stepping/custom/syntheticProvider.kt b/idea/testData/debugger/tinyApp/src/stepping/custom/syntheticProvider.kt new file mode 100644 index 00000000000..7a359c7d8ed --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/stepping/custom/syntheticProvider.kt @@ -0,0 +1,59 @@ +package syntheticProvider + +interface I { + fun manyInts(i: Int, i2: Int, i3: Int, i4: Int): Int { + return i + } + + fun manyLongs(i: Long, i2: Long, i3: Long, i4: Long): Long { + return i + } + + fun manyDouble(i: Double, i2: Double, i3: Double, i4: Double): Double { + return i + } + + fun manyFloat(i: Float, i2: Float, i3: Float, i4: Float): Float { + return i + } + + fun manyObject(i: Any, i2: Any, i3: Any, i4: Any): Any { + return i + } + + fun void() { + val a = 1 + } +} + +class IImpl: I + +fun main(args: Array) { + val i = IImpl() + i.manyInts(1, 1, 1, 1) + + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + i.manyInts(1, 1, 1, 1) + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + i.manyLongs(1, 1, 1, 1) + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + i.manyDouble(1.0, 1.0, 1.0, 1.0) + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + i.manyFloat(1.0f, 1.0f, 1.0f, 1.0f) + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + i.manyObject(1, 1, 1, 1) + // STEP_INTO: 1 + // RESUME: 1 + //Breakpoint! + i.void() +} \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/stepping/stepInto/traits.kt b/idea/testData/debugger/tinyApp/src/stepping/stepInto/traits.kt index e7c5ae5dd8b..dba8519cd00 100644 --- a/idea/testData/debugger/tinyApp/src/stepping/stepInto/traits.kt +++ b/idea/testData/debugger/tinyApp/src/stepping/stepInto/traits.kt @@ -10,11 +10,11 @@ fun main(args: Array) { val o = object: MyInterface { override fun inInterfaceOverride(): Int { - return super.inInterfaceOverride() + return 1 } override val propInInterfaceOverride: Int - get() = super.propInInterfaceOverride + get() = 1 } o.inInterface() o.propInInterface diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java index f0b9b1e7d1c..efd0076f989 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java @@ -595,6 +595,12 @@ public class KotlinSteppingTestGenerated extends AbstractKotlinSteppingTest { doCustomTest(fileName); } + @TestMetadata("smartStepIntoInterfaceImpl.kt") + public void testSmartStepIntoInterfaceImpl() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/stepping/custom/smartStepIntoInterfaceImpl.kt"); + doCustomTest(fileName); + } + @TestMetadata("stepIntoStdlibInlineFun2step.kt") public void testStepIntoStdlibInlineFun2step() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/stepping/custom/stepIntoStdlibInlineFun2step.kt"); @@ -606,5 +612,11 @@ public class KotlinSteppingTestGenerated extends AbstractKotlinSteppingTest { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/stepping/custom/stepOutInlineFunctionStdlib.kt"); doCustomTest(fileName); } + + @TestMetadata("syntheticProvider.kt") + public void testSyntheticProvider() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/stepping/custom/syntheticProvider.kt"); + doCustomTest(fileName); + } } }