From 3d3bde5483f841744ccdfd1b247307c3941be627 Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Mon, 22 Jul 2019 18:44:56 +0900 Subject: [PATCH] Debugger: Do not ignore duplicated locations for line breakpoints --- .../breakpoints/KotlinLineBreakpoint.kt | 4 +- .../breakpoints/KotlinLineBreakpointBase.java | 230 ++++++++++++++++++ .../stepOverInlineFunWithRecursionCall.kt | 2 +- .../stepOverInlineFunWithRecursionCall.out | 1 + 4 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpointBase.java diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpoint.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpoint.kt index 80be603d9dd..d59d9c0f3f4 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpoint.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpoint.kt @@ -7,21 +7,19 @@ package org.jetbrains.kotlin.idea.debugger.breakpoints import com.intellij.debugger.engine.DebugProcess import com.intellij.debugger.impl.DebuggerUtilsEx -import com.intellij.debugger.ui.breakpoints.LineBreakpoint import com.intellij.openapi.project.Project import com.intellij.xdebugger.XSourcePosition import com.intellij.xdebugger.breakpoints.XBreakpoint import com.intellij.xdebugger.breakpoints.XBreakpointProperties import com.sun.jdi.AbsentInformationException import com.sun.jdi.ReferenceType -import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME import org.jetbrains.kotlin.idea.debugger.isDexDebug class KotlinLineBreakpoint( project: Project?, xBreakpoint: XBreakpoint>? -) : LineBreakpoint(project, xBreakpoint) { +) : KotlinLineBreakpointBase(project, xBreakpoint) { override fun processClassPrepare(debugProcess: DebugProcess?, classType: ReferenceType?) { val sourcePosition = xBreakpoint?.sourcePosition diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpointBase.java b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpointBase.java new file mode 100644 index 00000000000..ccbcef3e983 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/breakpoints/KotlinLineBreakpointBase.java @@ -0,0 +1,230 @@ +/* + * 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.SourcePosition; +import com.intellij.debugger.engine.DebugProcessImpl; +import com.intellij.debugger.engine.requests.RequestManagerImpl; +import com.intellij.debugger.impl.DebuggerUtilsEx; +import com.intellij.debugger.ui.breakpoints.FilteredRequestor; +import com.intellij.debugger.ui.breakpoints.LineBreakpoint; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiFile; +import com.intellij.psi.search.EverythingGlobalScope; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.xdebugger.breakpoints.XBreakpoint; +import com.sun.jdi.ClassNotPreparedException; +import com.sun.jdi.Location; +import com.sun.jdi.ObjectCollectedException; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.request.BreakpointRequest; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties; +import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +// Copied from com.intellij.debugger.ui.breakpoints.LineBreakpoint. +// Changed parts are marked with '// MODIFICATION: ' comments. +// This should be deleted when IDEA replaces the 'MethodBytecodeUtil.removeSameLineLocations' with overload-able method call. +public abstract class KotlinLineBreakpointBase extends LineBreakpoint { + private static final Logger LOG = Logger.getInstance(KotlinLineBreakpointBase.class); + + protected KotlinLineBreakpointBase(Project project, XBreakpoint xBreakpoint) { + super(project, xBreakpoint); + } + + @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(final DebugProcessImpl debugProcess, final ReferenceType classType) { + if (!ReadAction.compute(() -> isInScopeOf(debugProcess, classType.name()))) { + if (LOG.isDebugEnabled()) { + LOG.debug(classType.name() + " is out of debug-process scope, breakpoint request won't be created for line " + getLineIndex()); + } + return; + } + try { + List locations = debugProcess.getPositionManager().locationsOfLine(classType, getSourcePosition()); + if (!locations.isEmpty()) { + locations = StreamEx.of(locations).peek(loc -> { + if (LOG.isDebugEnabled()) { + LOG.debug("Found location [codeIndex=" + loc.codeIndex() + + "] for reference type " + classType.name() + + " at line " + getLineIndex() + + "; isObsolete: " + (debugProcess.getVirtualMachineProxy().versionHigher("1.4") && loc.method().isObsolete())); + } + }).filter(l -> acceptLocation(debugProcess, classType, l)).toList(); + // MODIFICATION: Start Kotlin implementation + //locations = MethodBytecodeUtil.removeSameLineLocations(locations); + // MODIFICATION: End Kotlin implementation + for (Location loc : locations) { + createLocationBreakpointRequest(this, loc, debugProcess); + if (LOG.isDebugEnabled()) { + LOG.debug("Created breakpoint request for reference type " + classType.name() + " at line " + getLineIndex() + "; codeIndex=" + loc.codeIndex()); + } + } + } + else if (DebuggerUtilsEx.allLineLocations(classType) == null) { + // there's no line info in this class + debugProcess.getRequestsManager() + .setInvalid(this, DebuggerBundle.message("error.invalid.breakpoint.no.line.info", classType.name())); + if (LOG.isDebugEnabled()) { + LOG.debug("No line number info in " + classType.name()); + } + } + else { + // there's no executable code in this class + debugProcess.getRequestsManager().setInvalid(this, DebuggerBundle.message( + "error.invalid.breakpoint.no.executable.code", (getLineIndex() + 1), classType.name()) + ); + if (LOG.isDebugEnabled()) { + LOG.debug("No locations of type " + classType.name() + " found at line " + getLineIndex()); + } + } + } + catch (ClassNotPreparedException ex) { + if (LOG.isDebugEnabled()) { + LOG.debug("ClassNotPreparedException: " + ex.getMessage()); + } + // there's a chance to add a breakpoint when the class is prepared + } + catch (ObjectCollectedException ex) { + if (LOG.isDebugEnabled()) { + LOG.debug("ObjectCollectedException: " + ex.getMessage()); + } + // there's a chance to add a breakpoint when the class is prepared + } + catch(Exception ex) { + LOG.info(ex); + } + updateUI(); + } + + private boolean isInScopeOf(DebugProcessImpl debugProcess, String className) { + final SourcePosition position = getSourcePosition(); + if (position != null) { + final VirtualFile breakpointFile = position.getFile().getVirtualFile(); + final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex(); + if (breakpointFile != null && fileIndex.isUnderSourceRootOfType(breakpointFile, JavaModuleSourceRootTypes.SOURCES)) { + if (debugProcess.getSearchScope().contains(breakpointFile)) { + return true; + } + // apply filtering to breakpoints from content sources only, not for sources attached to libraries + final Collection candidates = findClassCandidatesInSourceContent(className, debugProcess.getSearchScope(), fileIndex); + if (LOG.isDebugEnabled()) { + LOG.debug("Found "+ (candidates == null? "null" : candidates.size()) + " candidate containing files for class " + className); + } + if (candidates == null) { + // If no candidates are found in scope then assume that class is loaded dynamically and allow breakpoint + return true; + } + + // breakpointFile is not in scope here and there are some candidates in scope + //for (VirtualFile classFile : candidates) { + // if (LOG.isDebugEnabled()) { + // LOG.debug("Breakpoint file: " + breakpointFile.getPath()+ "; candidate file: " + classFile.getPath()); + // } + // if (breakpointFile.equals(classFile)) { + // return true; + // } + //} + if (LOG.isDebugEnabled()) { + final GlobalSearchScope scope = debugProcess.getSearchScope(); + final boolean contains = scope.contains(breakpointFile); + List files = ContainerUtil.map( + JavaPsiFacade.getInstance(myProject).findClasses(className, scope), + aClass -> aClass.getContainingFile().getVirtualFile()); + List allFiles = ContainerUtil.map( + JavaPsiFacade.getInstance(myProject).findClasses(className, new EverythingGlobalScope(myProject)), + aClass -> aClass.getContainingFile().getVirtualFile()); + final VirtualFile contentRoot = fileIndex.getContentRootForFile(breakpointFile); + final Module module = fileIndex.getModuleForFile(breakpointFile); + + LOG.debug("Did not find '" + + className + "' in " + scope + + "; contains=" + contains + + "; contentRoot=" + contentRoot + + "; module = " + module + + "; all files in index are: " + files+ + "; all possible files are: " + allFiles + ); + } + + return false; + } + } + return true; + } + + @Nullable + private Collection findClassCandidatesInSourceContent(final String className, final GlobalSearchScope scope, final ProjectFileIndex fileIndex) { + final int dollarIndex = className.indexOf("$"); + final String topLevelClassName = dollarIndex >= 0? className.substring(0, dollarIndex) : className; + return ReadAction.compute(() -> { + final PsiClass[] classes = JavaPsiFacade.getInstance(myProject).findClasses(topLevelClassName, scope); + if (LOG.isDebugEnabled()) { + LOG.debug("Found "+ classes.length + " classes " + topLevelClassName + " in scope "+scope); + } + if (classes.length == 0) { + return null; + } + final List list = new ArrayList<>(classes.length); + for (PsiClass aClass : classes) { + final PsiFile psiFile = aClass.getContainingFile(); + + if (LOG.isDebugEnabled()) { + final StringBuilder msg = new StringBuilder(); + msg.append("Checking class ").append(aClass.getQualifiedName()); + msg.append("\n\t").append("PsiFile=").append(psiFile); + if (psiFile != null) { + final VirtualFile vFile = psiFile.getVirtualFile(); + msg.append("\n\t").append("VirtualFile=").append(vFile); + if (vFile != null) { + msg.append("\n\t").append("isInSourceContent=").append(fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES)); + } + } + LOG.debug(msg.toString()); + } + + if (psiFile == null) { + return null; + } + final VirtualFile vFile = psiFile.getVirtualFile(); + if (vFile == null || !fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES)) { + return null; // this will switch off the check if at least one class is from libraries + } + list.add(vFile); + } + return list; + }); + } +} diff --git a/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.kt b/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.kt index b913fa68a3f..a04bebf526a 100644 --- a/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.kt +++ b/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.kt @@ -18,4 +18,4 @@ inline fun inlineCall(l: () -> Unit) { l() } -// STEP_OVER: 3 \ No newline at end of file +// STEP_OVER: 4 \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.out b/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.out index 9c8421b40ce..17eddeec2ec 100644 --- a/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.out +++ b/idea/testData/debugger/tinyApp/src/stepping/stepOver/stepOverInlineFunWithRecursionCall.out @@ -2,6 +2,7 @@ LineBreakpoint created at stepOverInlineFunWithRecursionCall.kt:6 Run Java Connected to the target VM stepOverInlineFunWithRecursionCall.kt:6 +stepOverInlineFunWithRecursionCall.kt:6 stepOverInlineFunWithRecursionCall.kt:9 stepOverInlineFunWithRecursionCall.kt:14 stepOverInlineFunWithRecursionCall.kt:15