From 22c18ffaa974c3bc55b83c7acc38abfc06094c89 Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Wed, 3 Jul 2019 18:25:54 +0900 Subject: [PATCH] Debugger: Add Kotlin Function breakpoints (KT-22116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now it's possible to put a function breakpoint. In JVM, function breakpoints behave as JVM method breakpoints. Normally, they're triggered twice – once on enter, and once on exit. --- .../kotlin/idea/KotlinBundle.properties | 1 + .../KotlinBreakpointHandlerFactory.kt | 11 +- .../breakpoints/KotlinFunctionBreakpoint.java | 667 ++++++++++++++++++ .../KotlinFunctionBreakpointType.java | 166 +++++ .../breakpoints/breakpointTypeUtils.kt | 74 +- idea/resources/META-INF/jvm.xml | 2 + idea/resources/META-INF/jvm.xml.182 | 2 + idea/resources/META-INF/jvm.xml.183 | 2 + 8 files changed, 920 insertions(+), 5 deletions(-) create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpoint.java create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpointType.java diff --git a/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties b/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties index 6150754eae1..5d1d0fded35 100644 --- a/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties +++ b/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties @@ -242,6 +242,7 @@ kotlin.compiler.js.option.output.copy.dir=O&utput directory for library &runtime debugger.filter.ignore.internal.classes=Do not step into Kotlin runtime library implementation classes debugger.data.view.delegated.properties=Calculate values of delegated properties (may affect program execution) debugger.field.watchpoints.tab.title=Kotlin Field Watchpoints +debugger.function.breakpoints.tab.title=Kotlin Function Breakpoints debugger.field.watchpoints.properties.panel.field.access.label=Field &access debugger.field.watchpoints.properties.panel.field.modification.label=Field &modification debugger.field.watchpoints.properties.panel.field.initialization.label=Field &initialization diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinBreakpointHandlerFactory.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinBreakpointHandlerFactory.kt index 8a92b551af0..1b984c50411 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinBreakpointHandlerFactory.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinBreakpointHandlerFactory.kt @@ -26,11 +26,18 @@ class KotlinFieldBreakpointHandlerFactory : JavaBreakpointHandlerFactory { } } -class KotlinLineBreakpointHandlerFactory: JavaBreakpointHandlerFactory { +class KotlinLineBreakpointHandlerFactory : JavaBreakpointHandlerFactory { override fun createHandler(process: DebugProcessImpl): JavaBreakpointHandler? { return KotlinLineBreakpointHandler(process) } } +class KotlinFunctionBreakpointHandlerFactory : JavaBreakpointHandlerFactory { + override fun createHandler(process: DebugProcessImpl): JavaBreakpointHandler? { + return KotlinFunctionBreakpointHandler(process) + } +} + class KotlinFieldBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinFieldBreakpointType::class.java, process) -class KotlinLineBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinLineBreakpointType::class.java, process) \ No newline at end of file +class KotlinLineBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinLineBreakpointType::class.java, process) +class KotlinFunctionBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinFunctionBreakpointType::class.java, process) \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpoint.java b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpoint.java new file mode 100644 index 00000000000..c8c0d715a4b --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpoint.java @@ -0,0 +1,667 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.breakpoints; + +import com.intellij.debugger.DebuggerBundle; +import com.intellij.debugger.DebuggerManagerEx; +import com.intellij.debugger.SourcePosition; +import com.intellij.debugger.engine.*; +import com.intellij.debugger.engine.evaluation.EvaluateException; +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl; +import com.intellij.debugger.engine.requests.RequestManagerImpl; +import com.intellij.debugger.impl.DebuggerUtilsEx; +import com.intellij.debugger.impl.DebuggerUtilsImpl; +import com.intellij.debugger.impl.PositionUtil; +import com.intellij.debugger.jdi.ClassesByNameProvider; +import com.intellij.debugger.jdi.MethodBytecodeUtil; +import com.intellij.debugger.requests.Requestor; +import com.intellij.debugger.ui.breakpoints.*; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.util.ProgressWindow; +import com.intellij.openapi.project.IndexNotReadyException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.JDOMExternalizerUtil; +import com.intellij.openapi.util.Key; +import com.intellij.psi.*; +import com.intellij.ui.LayeredIcon; +import com.intellij.util.DocumentUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; +import com.intellij.xdebugger.breakpoints.XBreakpoint; +import com.intellij.xdebugger.breakpoints.XBreakpointListener; +import com.intellij.xdebugger.impl.XDebugSessionImpl; +import com.sun.jdi.*; +import com.sun.jdi.event.LocatableEvent; +import com.sun.jdi.event.MethodEntryEvent; +import com.sun.jdi.event.MethodExitEvent; +import com.sun.jdi.request.*; +import one.util.streamex.StreamEx; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.java.debugger.breakpoints.properties.JavaMethodBreakpointProperties; +import org.jetbrains.kotlin.asJava.LightClassUtilsKt; +import org.jetbrains.kotlin.psi.KtClass; +import org.jetbrains.kotlin.psi.KtDeclaration; +import org.jetbrains.org.objectweb.asm.Label; +import org.jetbrains.org.objectweb.asm.MethodVisitor; +import org.jetbrains.org.objectweb.asm.Opcodes; + +import javax.swing.*; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +// This class is copied from com.intellij.debugger.ui.breakpoints.MethodBreakpoint. +// Changed parts are marked with '// MODIFICATION: ' comments. +// This should be deleted when IDEA opens the method breakpoint API (presumably in 193). +public class KotlinFunctionBreakpoint extends BreakpointWithHighlighter implements MethodBreakpointBase { + private static final Logger LOG = Logger.getInstance(KotlinFunctionBreakpoint.class); + @Nullable private JVMName mySignature; + private boolean myIsStatic; + + protected KotlinFunctionBreakpoint(@NotNull Project project, XBreakpoint breakpoint) { + super(project, breakpoint); + } + + public boolean isStatic() { + return myIsStatic; + } + + @Override + @NotNull + public Key getCategory() { + return MethodBreakpoint.CATEGORY; + } + + @Override + public boolean isValid() { + return super.isValid() && getMethodName() != null; + } + + @Override + public void reload() { + super.reload(); + + setMethodName(null); + mySignature = null; + + SourcePosition sourcePosition = getSourcePosition(); + if (sourcePosition != null) { + MethodDescriptor descriptor = getMethodDescriptor(myProject, sourcePosition); + if (descriptor != null) { + setMethodName(descriptor.methodName); + mySignature = descriptor.methodSignature; + myIsStatic = descriptor.isStatic; + } + } + PsiClass psiClass = getPsiClass(); + if (psiClass != null) { + getProperties().myClassPattern = psiClass.getQualifiedName(); + } + if (myIsStatic) { + setInstanceFiltersEnabled(false); + } + } + + private static void createRequestForSubClasses(@NotNull MethodBreakpointBase breakpoint, + @NotNull DebugProcessImpl debugProcess, + @NotNull ReferenceType baseType) { + DebuggerManagerThreadImpl.assertIsManagerThread(); + RequestManagerImpl requestsManager = debugProcess.getRequestsManager(); + ClassPrepareRequest request = requestsManager.createClassPrepareRequest((debuggerProcess, referenceType) -> { + if (DebuggerUtilsImpl.instanceOf(referenceType, baseType)) { + createRequestForPreparedClassEmulated(breakpoint, debugProcess, referenceType, false); + } + }, null); + if (request != null) { + requestsManager.registerRequest(breakpoint, request); + request.enable(); + } + + AtomicReference indicatorRef = new AtomicReference<>(); + ApplicationManager.getApplication().invokeAndWait( + () -> { + ProgressWindow progress = + new ProgressWindow(true, false, debugProcess.getProject(), "Cancel emulation"); + progress.setDelayInMillis(2000); + indicatorRef.set(progress); + }); + ProgressWindow indicator = indicatorRef.get(); + + AtomicBoolean changed = new AtomicBoolean(); + XBreakpointListener> listener = new XBreakpointListener>() { + void changed(@NotNull XBreakpoint b) { + if (b == breakpoint.getXBreakpoint()) { + changed.set(true); + indicator.cancel(); + } + } + + @Override + public void breakpointRemoved(@NotNull XBreakpoint b) { + changed(b); + } + + @Override + public void breakpointChanged(@NotNull XBreakpoint b) { + changed(b); + } + }; + + debugProcess.getProject().getMessageBus().connect(indicator).subscribe(XBreakpointListener.TOPIC, listener); + ProgressManager.getInstance().executeProcessUnderProgress( + () -> processPreparedSubTypes(baseType, + (subType, classesByName) -> + createRequestForPreparedClassEmulated(breakpoint, debugProcess, subType, classesByName, false), + indicator), + indicator); + if (indicator.isCanceled() && !changed.get()) { + breakpoint.disableEmulation(); + } + } + + @Override + public void disableEmulation() { + MethodBreakpointBase.disableEmulation(this); + } + + private static void createRequestForPreparedClassEmulated( + @NotNull MethodBreakpointBase breakpoint, + @NotNull DebugProcessImpl debugProcess, + @NotNull ReferenceType classType, + boolean base + ) { + createRequestForPreparedClassEmulated(breakpoint, debugProcess, classType, debugProcess.getVirtualMachineProxy().getClassesByNameProvider(), base); + } + + private static boolean shouldCreateRequest( + Requestor requestor, + XBreakpoint xBreakpoint, + DebugProcessImpl debugProcess, + boolean forPreparedClass + ) { + return ReadAction.compute(() -> { + JavaDebugProcess process = debugProcess.getXdebugProcess(); + return process != null + && debugProcess.isAttached() + && (xBreakpoint == null || ((XDebugSessionImpl)process.getSession()).isBreakpointActive(xBreakpoint)) + && (forPreparedClass || debugProcess.getRequestsManager().findRequests(requestor).isEmpty()); + }); + } + + private static void createRequestForPreparedClassEmulated( + @NotNull MethodBreakpointBase breakpoint, + @NotNull DebugProcessImpl debugProcess, + @NotNull ReferenceType classType, + @NotNull ClassesByNameProvider classesByName, + boolean base + ) { + if (!MethodBreakpointBase.canBeEmulated(debugProcess)) { + breakpoint.disableEmulation(); + return; + } + if (!base && !shouldCreateRequest(breakpoint, breakpoint.getXBreakpoint(), debugProcess, true)) { + return; + } + Method lambdaMethod = MethodBytecodeUtil.getLambdaMethod(classType, classesByName); + if (lambdaMethod != null && + !breakpoint + .matchingMethods(StreamEx.of(((ClassType)classType).interfaces()).flatCollection(ReferenceType::allMethods), debugProcess) + .findFirst().isPresent()) { + return; + } + StreamEx methods = lambdaMethod != null + ? StreamEx.of(lambdaMethod) + : breakpoint.matchingMethods(StreamEx.of(classType.methods()).filter(m -> base || !m.isAbstract()), debugProcess); + boolean found = false; + for (Method method : methods) { + found = true; + if (method.isNative()) { + breakpoint.disableEmulation(); + return; + } + Method target = MethodBytecodeUtil.getBridgeTargetMethod(method, classesByName); + if (target != null && !ContainerUtil.isEmpty(DebuggerUtilsEx.allLineLocations(target))) { + method = target; + } + + List allLineLocations = DebuggerUtilsEx.allLineLocations(method); + if (allLineLocations == null && !method.isBridge()) { // no line numbers + breakpoint.disableEmulation(); + return; + } + if (!ContainerUtil.isEmpty(allLineLocations)) { + if (breakpoint.isWatchEntry()) { + createLocationBreakpointRequest(breakpoint, ContainerUtil.getFirstItem(allLineLocations), debugProcess, true); + } + if (breakpoint.isWatchExit()) { + MethodBytecodeUtil.visit(method, new MethodVisitor(Opcodes.API_VERSION) { + int myLastLine = 0; + @Override + public void visitLineNumber(int line, Label start) { + myLastLine = line; + } + + @Override + public void visitInsn(int opcode) { + switch (opcode) { + case Opcodes.RETURN: + case Opcodes.IRETURN: + case Opcodes.FRETURN: + case Opcodes.ARETURN: + case Opcodes.LRETURN: + case Opcodes.DRETURN: + //case Opcodes.ATHROW: + allLineLocations.stream() + .filter(l -> l.lineNumber() == myLastLine) + .findFirst().ifPresent(location -> createLocationBreakpointRequest(breakpoint, location, debugProcess, false)); + } + } + }, true); + } + } + } + if (base && found) { + // desired class found - now also track all new classes + createRequestForSubClasses(breakpoint, debugProcess, classType); + } + } + + private static void createLocationBreakpointRequest(@NotNull FilteredRequestor requestor, + @Nullable Location location, + @NotNull DebugProcessImpl debugProcess, + boolean methodEntry) { + BreakpointRequest request = createLocationBreakpointRequest(requestor, location, debugProcess); + if (request != null) { + request.putProperty(METHOD_ENTRY_KEY, methodEntry); + } + } + + @Nullable + private static BreakpointRequest createLocationBreakpointRequest( + @NotNull FilteredRequestor requestor, + @Nullable Location location, + @NotNull DebugProcessImpl debugProcess + ) { + if (location != null) { + RequestManagerImpl requestsManager = debugProcess.getRequestsManager(); + BreakpointRequest request = requestsManager.createBreakpointRequest(requestor, location); + requestsManager.enableRequest(request); + return request; + } + return null; + } + + @Override + protected void createRequestForPreparedClass(@NotNull DebugProcessImpl debugProcess, @NotNull ReferenceType classType) { + if (isEmulated()) { + createRequestForPreparedClassEmulated(this, debugProcess, classType, true); + } + else { + createRequestForPreparedClassOriginal(debugProcess, classType); + } + } + + private void createRequestForPreparedClassOriginal(@NotNull DebugProcessImpl debugProcess, @NotNull ReferenceType classType) { + try { + boolean hasMethod = false; + for (Method method : classType.allMethods()) { + String signature = method.signature(); + String name = method.name(); + + String nameFromProperties = getMethodName(); + if (nameFromProperties != null && mySignature != null) { + if (nameFromProperties.equals(name) && mySignature.getName(debugProcess).equals(signature)) { + hasMethod = true; + break; + } + } + } + + if(!hasMethod) { + debugProcess.getRequestsManager().setInvalid( + this, DebuggerBundle.message("error.invalid.breakpoint.method.not.found", classType.name()) + ); + return; + } + + RequestManagerImpl requestManager = debugProcess.getRequestsManager(); + if (isWatchEntry()) { + MethodEntryRequest entryRequest = findRequest(debugProcess, MethodEntryRequest.class, this); + if (entryRequest == null) { + entryRequest = requestManager.createMethodEntryRequest(this); + } + else { + entryRequest.disable(); + } + //entryRequest.addClassFilter(myClassQualifiedName); + // use addClassFilter(ReferenceType) in order to stop on subclasses also! + entryRequest.addClassFilter(classType); + debugProcess.getRequestsManager().enableRequest(entryRequest); + } + if (isWatchExit()) { + MethodExitRequest exitRequest = findRequest(debugProcess, MethodExitRequest.class, this); + if (exitRequest == null) { + exitRequest = requestManager.createMethodExitRequest(this); + } + else { + exitRequest.disable(); + } + //exitRequest.addClassFilter(myClassQualifiedName); + exitRequest.addClassFilter(classType); + debugProcess.getRequestsManager().enableRequest(exitRequest); + } + } + catch (Exception e) { + LOG.debug(e); + } + } + + @Override + public String getEventMessage(@NotNull LocatableEvent event) { + return getEventMessage(event, getFileName()); + } + + private static String getEventMessage(@NotNull LocatableEvent event, @NotNull String defaultFileName) { + Location location = event.location(); + if (event instanceof MethodEntryEvent) { + return getEventMessage(true, ((MethodEntryEvent)event).method(), location, defaultFileName); + } + if (event instanceof MethodExitEvent) { + return getEventMessage(false, ((MethodExitEvent)event).method(), location, defaultFileName); + } + Object entryProperty = event.request().getProperty(METHOD_ENTRY_KEY); + if (entryProperty instanceof Boolean) { + return getEventMessage((Boolean)entryProperty, location.method(), location, defaultFileName); + } + return ""; + } + + private static String getEventMessage(boolean entry, Method method, Location location, String defaultFileName) { + String locationQName = DebuggerUtilsEx.getLocationMethodQName(location); + String locationFileName = DebuggerUtilsEx.getSourceName(location, e -> defaultFileName); + int locationLine = location.lineNumber(); + return DebuggerBundle.message(entry ? "status.method.entry.breakpoint.reached" : "status.method.exit.breakpoint.reached", + method.declaringType().name() + "." + method.name() + "()", + locationQName, + locationFileName, + locationLine + ); + } + + @Override + public PsiElement getEvaluationElement() { + return getPsiClass(); + } + + @Override + protected Icon getDisabledIcon(boolean isMuted) { + if (DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().findMasterBreakpoint(this) != null && isMuted) { + return AllIcons.Debugger.Db_muted_dep_method_breakpoint; + } + return null; + } + + @Override + protected Icon getVerifiedIcon(boolean isMuted) { + return isSuspend() ? AllIcons.Debugger.Db_verified_method_breakpoint : AllIcons.Debugger.Db_verified_no_suspend_method_breakpoint; + } + + @Override + @NotNull + protected Icon getVerifiedWarningsIcon(boolean isMuted) { + return new LayeredIcon(isMuted ? AllIcons.Debugger.Db_muted_method_breakpoint : AllIcons.Debugger.Db_method_breakpoint, + AllIcons.General.WarningDecorator); + } + + @Override + public String getDisplayName() { + StringBuilder buffer = new StringBuilder(); + if(isValid()) { + String className = getClassName(); + boolean classNameExists = className != null && className.length() > 0; + if (classNameExists) { + buffer.append(className); + } + if(getMethodName() != null) { + if (classNameExists) { + buffer.append("."); + } + buffer.append(getMethodName()); + } + } + else { + buffer.append(DebuggerBundle.message("status.breakpoint.invalid")); + } + return buffer.toString(); + } + + @Override + public boolean evaluateCondition(@NotNull EvaluationContextImpl context, @NotNull LocatableEvent event) throws EvaluateException { + if (!matchesEvent(event, context.getDebugProcess())) { + return false; + } + return super.evaluateCondition(context, event); + } + + private boolean matchesEvent(@NotNull LocatableEvent event, DebugProcessImpl process) throws EvaluateException { + if (isEmulated()) { + return true; + } + if (getMethodName() == null || mySignature == null) { + return false; + } + Method method = event.location().method(); + return method != null && method.name().equals(getMethodName()) && method.signature().equals(mySignature.getName(process)); + } + + @Nullable + public static KotlinFunctionBreakpoint create(@NotNull Project project, XBreakpoint xBreakpoint) { + KotlinFunctionBreakpoint breakpoint = new KotlinFunctionBreakpoint(project, xBreakpoint); + return (KotlinFunctionBreakpoint)breakpoint.init(); + } + + //public boolean canMoveTo(final SourcePosition position) { + // return super.canMoveTo(position) && PositionUtil.getPsiElementAt(getProject(), PsiMethod.class, position) != null; + //} + + /** + * finds FQ method's class name and method's signature + */ + @Nullable + private static MethodDescriptor getMethodDescriptor(@NotNull Project project, @NotNull SourcePosition sourcePosition) { + Document document = PsiDocumentManager.getInstance(project).getDocument(sourcePosition.getFile()); + if (document == null) { + return null; + } + //final int endOffset = document.getLineEndOffset(sourcePosition); + //final MethodDescriptor descriptor = docManager.commitAndRunReadAction(new Computable() { + // conflicts with readAction on initial breakpoints creation + MethodDescriptor descriptor = ReadAction.compute(() -> { + // MODIFICATION: Start Kotlin implementation + PsiMethod method = resolveJvmMethodFromKotlinDeclaration(project, sourcePosition); + // MODIFICATION: End Kotlin implementation + if (method == null) { + return null; + } + int methodOffset = method.getTextOffset(); + if (!DocumentUtil.isValidOffset(methodOffset, document) || document.getLineNumber(methodOffset) < sourcePosition.getLine()) { + return null; + } + + PsiIdentifier identifier = method.getNameIdentifier(); + int methodNameOffset = identifier != null? identifier.getTextOffset() : methodOffset; + MethodDescriptor res = + new MethodDescriptor(); + res.methodName = JVMNameUtil.getJVMMethodName(method); + try { + res.methodSignature = JVMNameUtil.getJVMSignature(method); + res.isStatic = method.hasModifierProperty(PsiModifier.STATIC); + } + catch (IndexNotReadyException ignored) { + return null; + } + res.methodLine = document.getLineNumber(methodNameOffset); + return res; + }); + if (descriptor == null || descriptor.methodName == null || descriptor.methodSignature == null) { + return null; + } + return descriptor; + } + + // MODIFICATION: Start Kotlin implementation + @Nullable + private static PsiMethod resolveJvmMethodFromKotlinDeclaration(@NotNull Project project, @NotNull SourcePosition sourcePosition) { + KtDeclaration declaration = PositionUtil.getPsiElementAt(project, KtDeclaration.class, sourcePosition); + + if (declaration instanceof KtClass) { + declaration = ((KtClass) declaration).getPrimaryConstructor(); + } + + if (declaration == null) { + return null; + } + + for (PsiElement element : LightClassUtilsKt.toLightElements(declaration)) { + if (element instanceof PsiMethod) { + // TODO handle all light methods + return (PsiMethod) element; + } + } + + return null; + } + // MODIFICATION: End Kotlin implementation + + @Nullable + private static T findRequest( + @NotNull DebugProcessImpl debugProcess, + Class requestClass, + Requestor requestor + ) { + return StreamEx.of(debugProcess.getRequestsManager().findRequests(requestor)).select(requestClass).findFirst().orElse(null); + } + + @Override + public void readExternal(@NotNull Element breakpointNode) throws InvalidDataException { + super.readExternal(breakpointNode); + try { + getProperties().WATCH_ENTRY = Boolean.valueOf(JDOMExternalizerUtil.readField(breakpointNode, "WATCH_ENTRY")); + } catch (Exception ignored) { + } + try { + getProperties().WATCH_EXIT = Boolean.valueOf(JDOMExternalizerUtil.readField(breakpointNode, "WATCH_EXIT")); + } catch (Exception ignored) { + } + } + + private boolean isEmulated() { + return getProperties().EMULATED; + } + + @Override + public boolean isWatchEntry() { + return getProperties().WATCH_ENTRY; + } + + @Override + public boolean isWatchExit() { + return getProperties().WATCH_EXIT; + } + + @Override + public StreamEx matchingMethods(StreamEx methods, DebugProcessImpl debugProcess) { + try { + String methodName = getMethodName(); + String signature = mySignature != null ? mySignature.getName(debugProcess) : null; + return methods.filter(m -> Comparing.equal(methodName, m.name()) && Comparing.equal(signature, m.signature())).limit(1); + } + catch (EvaluateException e) { + LOG.warn(e); + } + return StreamEx.empty(); + } + + @Nullable + private String getMethodName() { + return getProperties().myMethodName; + } + + private void setMethodName(@Nullable String methodName) { + getProperties().myMethodName = methodName; + } + + private static final class MethodDescriptor { + String methodName; + JVMName methodSignature; + boolean isStatic; + int methodLine; + } + + private static void processPreparedSubTypes(ReferenceType classType, + BiConsumer consumer, + ProgressIndicator progressIndicator) { + long start = 0; + if (LOG.isDebugEnabled()) { + start = System.currentTimeMillis(); + } + progressIndicator.setIndeterminate(false); + progressIndicator.start(); + progressIndicator.setText(DebuggerBundle.message("label.method.breakpoints.processing.classes")); + try { + MultiMap inheritance = new MultiMap<>(); + List allTypes = classType.virtualMachine().allClasses(); + for (int i = 0; i < allTypes.size(); i++) { + if (progressIndicator.isCanceled()) { + return; + } + ReferenceType type = allTypes.get(i); + if (type.isPrepared()) { + try { + DebuggerUtilsImpl.supertypes(type).forEach(st -> inheritance.putValue(st, type)); + } + catch (ObjectCollectedException ignored) { + } + } + progressIndicator.setText2(i + "/" + allTypes.size()); + progressIndicator.setFraction((double)i / allTypes.size()); + } + List types = StreamEx.ofTree(classType, t -> StreamEx.of(inheritance.get(t))).skip(1).toList(); + + progressIndicator.setText(DebuggerBundle.message("label.method.breakpoints.setting.breakpoints")); + + ClassesByNameProvider classesByName = ClassesByNameProvider.createCache(allTypes); + + for (int i = 0; i < types.size(); i++) { + if (progressIndicator.isCanceled()) { + return; + } + consumer.accept(types.get(i), classesByName); + + progressIndicator.setText2(i + "/" + types.size()); + progressIndicator.setFraction((double)i / types.size()); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Processed " + types.size() + " classes in " + (System.currentTimeMillis() - start) + "ms"); + } + } + finally { + progressIndicator.stop(); + } + } +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpointType.java b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpointType.java new file mode 100644 index 00000000000..db08c034cbb --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinFunctionBreakpointType.java @@ -0,0 +1,166 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.breakpoints; + +import com.intellij.debugger.ui.breakpoints.Breakpoint; +import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointTypeBase; +import com.intellij.debugger.ui.breakpoints.MethodBreakpointPropertiesPanel; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.xdebugger.breakpoints.XBreakpoint; +import com.intellij.xdebugger.breakpoints.XLineBreakpoint; +import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.java.debugger.breakpoints.properties.JavaMethodBreakpointProperties; +import org.jetbrains.kotlin.idea.KotlinBundle; +import org.jetbrains.kotlin.psi.*; + +import javax.swing.*; + +import static org.jetbrains.kotlin.idea.debugger.breakpoints.BreakpointTypeUtilsKt.isBreakpointApplicable; + +// This class is copied from com.intellij.debugger.ui.breakpoints.MethodBreakpoint. +// Changed parts are marked with '// MODIFICATION: ' comments. +// This should be deleted when IDEA opens the method breakpoint API (presumably in 193). +public class KotlinFunctionBreakpointType extends JavaLineBreakpointTypeBase { + // MODIFICATION: Start Kotlin implementation + public KotlinFunctionBreakpointType() { + super("kotlin-function", KotlinBundle.message("debugger.function.breakpoints.tab.title")); + } + + @Override + public int getPriority() { + return 120; + } + // MODIFICATION: End Kotlin implementation + + @NotNull + @Override + public Icon getEnabledIcon() { + return AllIcons.Debugger.Db_method_breakpoint; + } + + @NotNull + @Override + public Icon getDisabledIcon() { + return AllIcons.Debugger.Db_disabled_method_breakpoint; + } + + @NotNull + @Override + public Icon getSuspendNoneIcon() { + return AllIcons.Debugger.Db_no_suspend_method_breakpoint; + } + + @NotNull + @Override + public Icon getMutedEnabledIcon() { + return AllIcons.Debugger.Db_muted_method_breakpoint; + } + + @NotNull + @Override + public Icon getMutedDisabledIcon() { + return AllIcons.Debugger.Db_muted_disabled_method_breakpoint; + } + + @NotNull + @Override + public Icon getInactiveDependentIcon() { + return AllIcons.Debugger.Db_dep_method_breakpoint; + } + + @Override + public String getShortText(XLineBreakpoint breakpoint) { + StringBuilder buffer = new StringBuilder(); + String className = breakpoint.getProperties().myClassPattern; + boolean classNameExists = className != null && className.length() > 0; + + if (classNameExists) { + buffer.append(className); + } + + if (breakpoint.getProperties().myMethodName != null) { + if (classNameExists) { + buffer.append("."); + } + buffer.append(breakpoint.getProperties().myMethodName); + } + + return buffer.toString(); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public XBreakpointCustomPropertiesPanel createCustomPropertiesPanel(@NotNull Project project) { + return new MethodBreakpointPropertiesPanel(); + } + + @Nullable + @Override + public JavaMethodBreakpointProperties createProperties() { + return new JavaMethodBreakpointProperties(); + } + + @Nullable + @Override + public JavaMethodBreakpointProperties createBreakpointProperties(@NotNull VirtualFile file, int line) { + JavaMethodBreakpointProperties properties = new JavaMethodBreakpointProperties(); + if (Registry.is("debugger.emulate.method.breakpoints")) { + properties.EMULATED = true; // create all new emulated + } + return properties; + } + + @NotNull + @Override + public Breakpoint createJavaBreakpoint(Project project, XBreakpoint breakpoint) { + return new KotlinFunctionBreakpoint(project, breakpoint); + } + + @Override + public boolean canBeHitInOtherPlaces() { + return true; + } + + // MODIFICATION: Start Kotlin implementation + @Override + public boolean canPutAt(@NotNull VirtualFile file, int line, @NotNull Project project) { + return isBreakpointApplicable(file, line, project, element -> { + if (element instanceof KtConstructor) { + return ApplicabilityResult.DEFINITELY_YES; + } + + if (element instanceof KtClass) { + KtClass clazz = (KtClass) element; + return ApplicabilityResult.maybe( + !(clazz instanceof KtEnumEntry) + && !clazz.isAnnotation() + && !clazz.isInterface() + && clazz.hasPrimaryConstructor() + ); + } + + if (element instanceof KtFunction) { + KtFunction function = (KtFunction) element; + return ApplicabilityResult.maybe(function.hasBody() && !KtPsiUtil.isLocal(function)); + } + + if (element instanceof KtPropertyAccessor) { + KtPropertyAccessor accessor = (KtPropertyAccessor) element; + KtProperty property = accessor.getProperty(); + return ApplicabilityResult.maybe(accessor.hasBody() && !KtPsiUtil.isLocal(property)); + } + + return ApplicabilityResult.UNKNOWN; + }); + } + // MODIFICATION: End Kotlin implementation +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/breakpointTypeUtils.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/breakpointTypeUtils.kt index 872a0926a26..de0990f9ce4 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/breakpointTypeUtils.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/breakpointTypeUtils.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.debugger.breakpoints import com.intellij.debugger.SourcePosition import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType +import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile @@ -34,9 +35,7 @@ import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils import org.jetbrains.kotlin.idea.core.util.getLineNumber import org.jetbrains.kotlin.idea.debugger.findElementAtLine import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.endOffset -import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf -import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import java.util.* @@ -85,6 +84,75 @@ fun canPutAt(file: VirtualFile, line: Int, project: Project, breakpointTypeClass return result == breakpointTypeClass } +class ApplicabilityResult(val isApplicable: Boolean, val shouldStop: Boolean) { + companion object { + @JvmStatic + fun definitely(result: Boolean) = ApplicabilityResult(result, shouldStop = true) + + @JvmStatic + fun maybe(result: Boolean) = ApplicabilityResult(result, shouldStop = false) + + @JvmField + val UNKNOWN = ApplicabilityResult(isApplicable = false, shouldStop = false) + + @JvmField + val DEFINITELY_YES = ApplicabilityResult(isApplicable = true, shouldStop = true) + + @JvmField + val MAYBE_YES = ApplicabilityResult(isApplicable = true, shouldStop = false) + } +} + +fun isBreakpointApplicable(file: VirtualFile, line: Int, project: Project, checker: (PsiElement) -> ApplicabilityResult): Boolean { + val psiFile = PsiManager.getInstance(project).findFile(file) + + if (psiFile == null || psiFile.virtualFile?.fileType != KotlinFileType.INSTANCE) { + return false + } + + val document = FileDocumentManager.getInstance().getDocument(file) ?: return false + var isApplicable = false + + val checked = HashSet() + + XDebuggerUtil.getInstance().iterateLine(project, document, line, fun(element: PsiElement): Boolean { + if (element is PsiWhiteSpace || element.getParentOfType(false) != null) { + return true + } + + val parent = getTopmostParentOnLineOrSelf(element, document, line) + if (!checked.add(parent)) { + return true + } + + val result = checker(parent) + + if (result.shouldStop && !result.isApplicable) { + isApplicable = false + return false + } + + isApplicable = isApplicable or result.isApplicable + return !result.shouldStop + }) + + return isApplicable +} + +private fun getTopmostParentOnLineOrSelf(element: PsiElement, document: Document, line: Int): PsiElement { + var current = element + var parent = current.parent + while (parent != null) { + val offset = parent.textOffset + if (offset >= 0 && document.getLineNumber(offset) != line) break + + current = parent + parent = current.parent + } + + return current +} + fun computeVariants( project: Project, position: XSourcePosition, diff --git a/idea/resources/META-INF/jvm.xml b/idea/resources/META-INF/jvm.xml index 7e9c96b9efc..62d9017bdff 100644 --- a/idea/resources/META-INF/jvm.xml +++ b/idea/resources/META-INF/jvm.xml @@ -106,9 +106,11 @@ + + diff --git a/idea/resources/META-INF/jvm.xml.182 b/idea/resources/META-INF/jvm.xml.182 index 85ecdbf2402..e5ddf2498dd 100644 --- a/idea/resources/META-INF/jvm.xml.182 +++ b/idea/resources/META-INF/jvm.xml.182 @@ -105,9 +105,11 @@ + + diff --git a/idea/resources/META-INF/jvm.xml.183 b/idea/resources/META-INF/jvm.xml.183 index 8811353c26b..76c6646b11f 100644 --- a/idea/resources/META-INF/jvm.xml.183 +++ b/idea/resources/META-INF/jvm.xml.183 @@ -106,9 +106,11 @@ + +