From ea8de883ac09e3b875e206ce366e1f30c1f92329 Mon Sep 17 00:00:00 2001 From: Natalia Ukhorskaya Date: Wed, 9 Dec 2015 16:05:02 +0300 Subject: [PATCH] Implement 'Skip simple getters' for debugger --- idea/src/META-INF/plugin.xml | 1 + .../idea/debugger/KotlinPositionManager.kt | 21 +++ .../extractFunctionForDebuggerUtil.kt | 152 ++++++++++-------- .../stepping/KotlinSimpleGetterProvider.kt | 54 +++++++ .../debugger/tinyApp/outs/onGetter.out | 9 ++ .../tinyApp/outs/skipSimpleGetter.out | 8 + .../src/evaluate/singleBreakpoint/onGetter.kt | 12 ++ .../src/stepping/stepInto/skipSimpleGetter.kt | 25 +++ .../debugger/KotlinSteppingTestGenerated.java | 6 + .../kotlin/idea/debugger/MockLocation.java | 2 +- .../kotlin/idea/debugger/MockMethod.kt | 59 +++++++ ...KotlinEvaluateExpressionTestGenerated.java | 6 + 12 files changed, 283 insertions(+), 72 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/debugger/stepping/KotlinSimpleGetterProvider.kt create mode 100644 idea/testData/debugger/tinyApp/outs/onGetter.out create mode 100644 idea/testData/debugger/tinyApp/outs/skipSimpleGetter.out create mode 100644 idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/onGetter.kt create mode 100644 idea/testData/debugger/tinyApp/src/stepping/stepInto/skipSimpleGetter.kt create mode 100644 idea/tests/org/jetbrains/kotlin/idea/debugger/MockMethod.kt diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 67e60cbd1e8..38a828bd98b 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -515,6 +515,7 @@ + diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt index c7ae0a309b8..96d7073c81a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt @@ -53,12 +53,15 @@ import org.jetbrains.kotlin.fileClasses.internalNameWithoutInnerClasses import org.jetbrains.kotlin.idea.caches.resolve.analyzeAndGetResult import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils +import org.jetbrains.kotlin.idea.core.refactoring.getLineStartOffset import org.jetbrains.kotlin.idea.debugger.breakpoints.getLambdasAtLineIfAny +import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory import org.jetbrains.kotlin.idea.decompiler.classFile.KtClsFile import org.jetbrains.kotlin.idea.search.usagesSearch.isImportUsage import org.jetbrains.kotlin.idea.util.DebuggerUtils import org.jetbrains.kotlin.idea.util.ProjectRootsUtil import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.load.java.JvmAbi import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.BindingContext @@ -125,12 +128,30 @@ public class KotlinPositionManager(private val myDebugProcess: DebugProcess) : M if (lambdaOrFunIfInside != null) { return SourcePosition.createFromElement(lambdaOrFunIfInside.bodyExpression!!) } + val property = getParameterIfInConstructor(location, psiFile, lineNumber) + if (property != null) { + return SourcePosition.createFromElement(property) + } return SourcePosition.createFromLine(psiFile, lineNumber) } throw NoDataException.INSTANCE } + private fun getParameterIfInConstructor(location: Location, file: KtFile, lineNumber: Int): KtParameter? { + val lineStartOffset = file.getLineStartOffset(lineNumber) ?: return null + val elementAt = file.findElementAt(lineStartOffset) + val contextElement = KotlinCodeFragmentFactory.getContextElement(elementAt) + val methodName = location.method().name() + if (contextElement is KtClass && JvmAbi.isGetterName(methodName)) { + val parameterForGetter = contextElement.getPrimaryConstructor()?.valueParameters?.firstOrNull() { + it.hasValOrVar() && it.name != null && JvmAbi.getterName(it.name!!) == methodName + } ?: return null + return parameterForGetter + } + return null + } + private fun getLambdaOrFunIfInside(location: Location, file: KtFile, lineNumber: Int): KtFunction? { val currentLocationFqName = location.declaringType().name() if (currentLocationFqName == null) return null diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/extractFunctionForDebuggerUtil.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/extractFunctionForDebuggerUtil.kt index 4097fa9a0be..741f5f16080 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/extractFunctionForDebuggerUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/extractFunctionForDebuggerUtil.kt @@ -180,81 +180,13 @@ private fun getExpressionToAddDebugExpressionBefore(tmpFile: KtFile, contextElem } private fun addDebugExpressionBeforeContextElement(codeFragment: KtCodeFragment, contextElement: PsiElement): List { - val psiFactory = KtPsiFactory(codeFragment) - - fun insertNewInitializer(classBody: KtClassBody): PsiElement? { - val initializer = psiFactory.createAnonymousInitializer() - val newInitializer = (classBody.addAfter(initializer, classBody.firstChild) as KtAnonymousInitializer) - val block = newInitializer.body as KtBlockExpression? - return block?.lastChild - } - - val elementBefore = when { - contextElement is KtFile -> { - val fakeFunction = psiFactory.createFunction("fun _debug_fun_() {}") - contextElement.add(psiFactory.createNewLine()) - val newFakeFun = contextElement.add(fakeFunction) as KtNamedFunction - newFakeFun.bodyExpression!!.lastChild - } - contextElement is KtProperty && !contextElement.isLocal -> { - val delegateExpressionOrInitializer = contextElement.delegateExpressionOrInitializer - if (delegateExpressionOrInitializer != null) { - wrapInRunFun(delegateExpressionOrInitializer) - } - else { - val getter = contextElement.getter!! - if (!getter.hasBlockBody()) { - wrapInRunFun(getter.bodyExpression!!) - } - else { - (getter.bodyExpression as KtBlockExpression).statements.first() - } - } - } - contextElement is KtPrimaryConstructor -> { - val classOrObject = contextElement.getContainingClassOrObject() - insertNewInitializer(classOrObject.getOrCreateBody()) - } - contextElement is KtClassOrObject -> { - insertNewInitializer(contextElement.getOrCreateBody()) - } - contextElement is KtFunctionLiteral -> { - val block = contextElement.bodyExpression!! - block.statements.firstOrNull() ?: block.lastChild - } - contextElement is KtDeclarationWithBody && !contextElement.hasBody()-> { - val block = psiFactory.createBlock("") - val newBlock = contextElement.add(block) as KtBlockExpression - newBlock.rBrace - } - contextElement is KtDeclarationWithBody && !contextElement.hasBlockBody()-> { - wrapInRunFun(contextElement.bodyExpression!!) - } - contextElement is KtDeclarationWithBody && contextElement.hasBlockBody()-> { - val block = contextElement.bodyExpression as KtBlockExpression - val last = block.statements.lastOrNull() - if (last is KtReturnExpression) - last - else - block.rBrace - } - contextElement is KtWhenEntry -> { - val entryExpression = contextElement.expression - if (entryExpression is KtBlockExpression) { - entryExpression.statements.firstOrNull() ?: entryExpression.lastChild - } - else { - wrapInRunFun(entryExpression!!) - } - } - else -> { - contextElement - } - } + val elementBefore = findElementBefore(contextElement) val parent = elementBefore?.parent if (parent == null || elementBefore == null) return emptyList() + val psiFactory = KtPsiFactory(codeFragment) + parent.addBefore(psiFactory.createNewLine(), elementBefore) fun insertExpression(expr: KtElement?): List { @@ -282,6 +214,84 @@ private fun addDebugExpressionBeforeContextElement(codeFragment: KtCodeFragment, return insertExpression(debugExpression) } +private fun findElementBefore(contextElement: PsiElement): PsiElement? { + val psiFactory = KtPsiFactory(contextElement) + + fun insertNewInitializer(classBody: KtClassBody): PsiElement? { + val initializer = psiFactory.createAnonymousInitializer() + val newInitializer = (classBody.addAfter(initializer, classBody.firstChild) as KtAnonymousInitializer) + val block = newInitializer.body as KtBlockExpression? + return block?.lastChild + } + + return when { + contextElement is KtFile -> { + val fakeFunction = psiFactory.createFunction("fun _debug_fun_() {}") + contextElement.add(psiFactory.createNewLine()) + val newFakeFun = contextElement.add(fakeFunction) as KtNamedFunction + newFakeFun.bodyExpression!!.lastChild + } + contextElement is KtProperty && !contextElement.isLocal -> { + val delegateExpressionOrInitializer = contextElement.delegateExpressionOrInitializer + if (delegateExpressionOrInitializer != null) { + wrapInRunFun(delegateExpressionOrInitializer) + } + else { + val getter = contextElement.getter!! + if (!getter.hasBlockBody()) { + wrapInRunFun(getter.bodyExpression!!) + } + else { + (getter.bodyExpression as KtBlockExpression).statements.first() + } + } + } + contextElement is KtParameter -> { + val ownerFunction = contextElement.ownerFunction!! + findElementBefore(ownerFunction) + } + contextElement is KtPrimaryConstructor -> { + val classOrObject = contextElement.getContainingClassOrObject() + insertNewInitializer(classOrObject.getOrCreateBody()) + } + contextElement is KtClassOrObject -> { + insertNewInitializer(contextElement.getOrCreateBody()) + } + contextElement is KtFunctionLiteral -> { + val block = contextElement.bodyExpression!! + block.statements.firstOrNull() ?: block.lastChild + } + contextElement is KtDeclarationWithBody && !contextElement.hasBody() -> { + val block = psiFactory.createBlock("") + val newBlock = contextElement.add(block) as KtBlockExpression + newBlock.rBrace + } + contextElement is KtDeclarationWithBody && !contextElement.hasBlockBody() -> { + wrapInRunFun(contextElement.bodyExpression!!) + } + contextElement is KtDeclarationWithBody && contextElement.hasBlockBody() -> { + val block = contextElement.bodyExpression as KtBlockExpression + val last = block.statements.lastOrNull() + if (last is KtReturnExpression) + last + else + block.rBrace + } + contextElement is KtWhenEntry -> { + val entryExpression = contextElement.expression + if (entryExpression is KtBlockExpression) { + entryExpression.statements.firstOrNull() ?: entryExpression.lastChild + } + else { + wrapInRunFun(entryExpression!!) + } + } + else -> { + contextElement + } + } +} + private fun replaceByRunFunction(expression: KtExpression): KtCallExpression { val callExpression = KtPsiFactory(expression).createExpression("run { \n${expression.text} \n}") as KtCallExpression val replaced = expression.replaced(callExpression) diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/stepping/KotlinSimpleGetterProvider.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/stepping/KotlinSimpleGetterProvider.kt new file mode 100644 index 00000000000..261bb6cdcbe --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/stepping/KotlinSimpleGetterProvider.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.debugger.stepping + +import com.intellij.debugger.engine.SimplePropertyGetterProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.psi.* + +class KotlinSimpleGetterProvider : SimplePropertyGetterProvider { + override fun isInsideSimpleGetter(element: PsiElement): Boolean { + // class A(val a: Int) + if (element is KtParameter) { + return true + } + + val accessor = PsiTreeUtil.getParentOfType(element, KtPropertyAccessor::class.java) + if (accessor != null && accessor.isGetter) { + val body = accessor.bodyExpression + return when (body) { + // val a: Int get() { return field } + is KtBlockExpression -> { + val returnedExpression = (body.statements.singleOrNull() as? KtReturnExpression)?.returnedExpression ?: return false + returnedExpression.textMatches("field") + } + // val a: Int get() = field + is KtExpression -> body.textMatches("field") + else -> false + } + } + + val property = PsiTreeUtil.getParentOfType(element, KtProperty::class.java) + // val a = foo() + if (property != null) { + return property.getter == null + } + + return false + } +} diff --git a/idea/testData/debugger/tinyApp/outs/onGetter.out b/idea/testData/debugger/tinyApp/outs/onGetter.out new file mode 100644 index 00000000000..6b89e8ff169 --- /dev/null +++ b/idea/testData/debugger/tinyApp/outs/onGetter.out @@ -0,0 +1,9 @@ +LineBreakpoint created at onGetter.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 !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! onGetter.OnGetterKt +Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' +onGetter.kt:9 +onGetter.kt:12 +Compile bytecode for prop +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/skipSimpleGetter.out b/idea/testData/debugger/tinyApp/outs/skipSimpleGetter.out new file mode 100644 index 00000000000..f01d3fec9f3 --- /dev/null +++ b/idea/testData/debugger/tinyApp/outs/skipSimpleGetter.out @@ -0,0 +1,8 @@ +LineBreakpoint created at skipSimpleGetter.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! skipSimpleGetter.SkipSimpleGetterKt +Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' +skipSimpleGetter.kt:6 +skipSimpleGetter.kt:7 +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/src/evaluate/singleBreakpoint/onGetter.kt b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/onGetter.kt new file mode 100644 index 00000000000..7c53b8bb642 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/onGetter.kt @@ -0,0 +1,12 @@ +package onGetter + +fun main(args: Array) { + val a = A(1) + // EXPRESSION: prop + // RESULT: 1: I + // STEP_INTO: 1 + //Breakpoint! + a.prop +} + +class A(val prop: Int) \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/stepping/stepInto/skipSimpleGetter.kt b/idea/testData/debugger/tinyApp/src/stepping/stepInto/skipSimpleGetter.kt new file mode 100644 index 00000000000..ab6bcf4ca9d --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/stepping/stepInto/skipSimpleGetter.kt @@ -0,0 +1,25 @@ +package skipSimpleGetter + +fun main(args: Array) { + val a = A(1) + //Breakpoint! + a.a1 + a.a2 + a.a3 + a.a4 +} + +class A(val a4: Int) { + // only init + val a1 = 1 + + // simple get, expression body + val a2: Int = 1 + get() = field + + // simple get, block body + val a3: Int = 1 + get() { + return field + } +} + +// STEP_INTO: 21 +// SKIP_GETTERS: true \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java index c518270e866..f0b9b1e7d1c 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java @@ -241,6 +241,12 @@ public class KotlinSteppingTestGenerated extends AbstractKotlinSteppingTest { doStepIntoTest(fileName); } + @TestMetadata("skipSimpleGetter.kt") + public void testSkipSimpleGetter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/stepping/stepInto/skipSimpleGetter.kt"); + doStepIntoTest(fileName); + } + @TestMetadata("stepIntoFromInlineFun.kt") public void testStepIntoFromInlineFun() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/stepping/stepInto/stepIntoFromInlineFun.kt"); diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/MockLocation.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/MockLocation.java index 08ce7265a94..276c3d506ba 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/MockLocation.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/MockLocation.java @@ -48,7 +48,7 @@ public class MockLocation implements Location { @Override public Method method() { - throw new UnsupportedOperationException(); + return new MockMethod(); } @Override diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/MockMethod.kt b/idea/tests/org/jetbrains/kotlin/idea/debugger/MockMethod.kt new file mode 100644 index 00000000000..71ed2fcdfda --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/MockMethod.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.debugger + +import com.sun.jdi.Method + +class MockMethod : Method { + override fun name() = "" + + override fun isSynthetic() = throw UnsupportedOperationException() + override fun isFinal() = throw UnsupportedOperationException() + override fun isStatic() = throw UnsupportedOperationException() + override fun declaringType() = throw UnsupportedOperationException() + override fun signature() = throw UnsupportedOperationException() + override fun genericSignature() = throw UnsupportedOperationException() + override fun variables() = throw UnsupportedOperationException() + override fun variablesByName(name: String?) = throw UnsupportedOperationException() + override fun bytecodes() = throw UnsupportedOperationException() + override fun isBridge() = throw UnsupportedOperationException() + override fun isObsolete() = throw UnsupportedOperationException() + override fun isSynchronized() = throw UnsupportedOperationException() + override fun allLineLocations() = throw UnsupportedOperationException() + override fun allLineLocations(stratum: String?, sourceName: String?) = throw UnsupportedOperationException() + override fun isNative() = throw UnsupportedOperationException() + override fun locationOfCodeIndex(codeIndex: Long) = throw UnsupportedOperationException() + override fun arguments() = throw UnsupportedOperationException() + override fun isAbstract() = throw UnsupportedOperationException() + override fun isVarArgs() = throw UnsupportedOperationException() + override fun returnTypeName() = throw UnsupportedOperationException() + override fun argumentTypes( ) = throw UnsupportedOperationException() + override fun isConstructor() = throw UnsupportedOperationException() + override fun locationsOfLine(lineNumber: Int) = throw UnsupportedOperationException() + override fun locationsOfLine(stratum: String?, sourceName: String?, lineNumber: Int) = throw UnsupportedOperationException() + override fun argumentTypeNames() = throw UnsupportedOperationException() + override fun returnType() = throw UnsupportedOperationException() + override fun isStaticInitializer() = throw UnsupportedOperationException() + override fun location() = throw UnsupportedOperationException() + override fun compareTo(other: Method?) = throw UnsupportedOperationException() + override fun isPackagePrivate() = throw UnsupportedOperationException() + override fun isPrivate() = throw UnsupportedOperationException() + override fun isProtected() = throw UnsupportedOperationException() + override fun isPublic() = throw UnsupportedOperationException() + override fun modifiers() = throw UnsupportedOperationException() + override fun virtualMachine() = throw UnsupportedOperationException() +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java index 81a59eb6e1c..d8c4d5ee9e1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java @@ -223,6 +223,12 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat doSingleBreakpointTest(fileName); } + @TestMetadata("onGetter.kt") + public void testOnGetter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/onGetter.kt"); + doSingleBreakpointTest(fileName); + } + @TestMetadata("onObjectHeader.kt") public void testOnObjectHeader() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/onObjectHeader.kt");