add utility method for containing method
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / ui / breakpoints / LineBreakpoint.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /*
18  * Class LineBreakpoint
19  * @author Jeka
20  */
21 package com.intellij.debugger.ui.breakpoints;
22
23 import com.intellij.debugger.DebuggerBundle;
24 import com.intellij.debugger.DebuggerManagerEx;
25 import com.intellij.debugger.SourcePosition;
26 import com.intellij.debugger.actions.ThreadDumpAction;
27 import com.intellij.debugger.engine.ContextUtil;
28 import com.intellij.debugger.engine.DebugProcessImpl;
29 import com.intellij.debugger.engine.DebuggerUtils;
30 import com.intellij.debugger.engine.evaluation.EvaluateException;
31 import com.intellij.debugger.engine.evaluation.EvaluationContextImpl;
32 import com.intellij.debugger.impl.DebuggerUtilsEx;
33 import com.intellij.debugger.jdi.StackFrameProxyImpl;
34 import com.intellij.icons.AllIcons;
35 import com.intellij.openapi.application.ApplicationManager;
36 import com.intellij.openapi.diagnostic.Logger;
37 import com.intellij.openapi.editor.Document;
38 import com.intellij.openapi.module.Module;
39 import com.intellij.openapi.project.Project;
40 import com.intellij.openapi.roots.ProjectFileIndex;
41 import com.intellij.openapi.roots.ProjectRootManager;
42 import com.intellij.openapi.util.Computable;
43 import com.intellij.openapi.util.Key;
44 import com.intellij.openapi.util.registry.Registry;
45 import com.intellij.openapi.vfs.VirtualFile;
46 import com.intellij.psi.*;
47 import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
48 import com.intellij.psi.jsp.JspFile;
49 import com.intellij.psi.search.EverythingGlobalScope;
50 import com.intellij.psi.search.GlobalSearchScope;
51 import com.intellij.psi.util.PsiTreeUtil;
52 import com.intellij.util.Function;
53 import com.intellij.util.Processor;
54 import com.intellij.util.StringBuilderSpinAllocator;
55 import com.intellij.util.containers.ContainerUtil;
56 import com.intellij.xdebugger.XDebuggerUtil;
57 import com.intellij.xdebugger.XSourcePosition;
58 import com.intellij.xdebugger.breakpoints.XBreakpoint;
59 import com.sun.jdi.*;
60 import com.sun.jdi.event.LocatableEvent;
61 import com.sun.jdi.request.BreakpointRequest;
62 import org.jetbrains.annotations.NonNls;
63 import org.jetbrains.annotations.NotNull;
64 import org.jetbrains.annotations.Nullable;
65 import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties;
66 import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes;
67
68 import javax.swing.*;
69 import java.util.ArrayList;
70 import java.util.Collection;
71 import java.util.List;
72 import java.util.regex.Pattern;
73
74 public class LineBreakpoint extends BreakpointWithHighlighter {
75   private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.ui.breakpoints.LineBreakpoint");
76
77   public static final @NonNls Key<LineBreakpoint> CATEGORY = BreakpointCategory.lookup("line_breakpoints");
78
79   protected LineBreakpoint(Project project, XBreakpoint xBreakpoint) {
80     super(project, xBreakpoint);
81   }
82
83   @Override
84   protected Icon getDisabledIcon(boolean isMuted) {
85     final Breakpoint master = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().findMasterBreakpoint(this);
86     if (isMuted) {
87       return master == null? AllIcons.Debugger.Db_muted_disabled_breakpoint : AllIcons.Debugger.Db_muted_dep_line_breakpoint;
88     }
89     else {
90       return master == null? AllIcons.Debugger.Db_disabled_breakpoint : AllIcons.Debugger.Db_dep_line_breakpoint;
91     }
92   }
93
94   @Override
95   protected Icon getSetIcon(boolean isMuted) {
96     if (isRemoveAfterHit()) {
97       return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint;
98     }
99     return isMuted? AllIcons.Debugger.Db_muted_breakpoint : AllIcons.Debugger.Db_set_breakpoint;
100   }
101
102   @Override
103   protected Icon getInvalidIcon(boolean isMuted) {
104     return isMuted? AllIcons.Debugger.Db_muted_invalid_breakpoint : AllIcons.Debugger.Db_invalid_breakpoint;
105   }
106
107   @Override
108   protected Icon getVerifiedIcon(boolean isMuted) {
109     if (isRemoveAfterHit()) {
110       return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint;
111     }
112     return isMuted? AllIcons.Debugger.Db_muted_verified_breakpoint : AllIcons.Debugger.Db_verified_breakpoint;
113   }
114
115   @Override
116   protected Icon getVerifiedWarningsIcon(boolean isMuted) {
117     return isMuted? AllIcons.Debugger.Db_muted_verified_warning_breakpoint : AllIcons.Debugger.Db_verified_warning_breakpoint;
118   }
119
120   @Override
121   public Key<LineBreakpoint> getCategory() {
122     return CATEGORY;
123   }
124
125   @Override
126   protected void createOrWaitPrepare(DebugProcessImpl debugProcess, String classToBeLoaded) {
127     if (isInScopeOf(debugProcess, classToBeLoaded)) {
128       super.createOrWaitPrepare(debugProcess, classToBeLoaded);
129     }
130   }
131
132   @Override
133   protected void createRequestForPreparedClass(final DebugProcessImpl debugProcess, final ReferenceType classType) {
134     if (!isInScopeOf(debugProcess, classType.name())) {
135       if (LOG.isDebugEnabled()) {
136         LOG.debug(classType.name() + " is out of debug-process scope, breakpoint request won't be created for line " + getLineIndex());
137       }
138       return;
139     }
140     try {
141       List<Location> locations = debugProcess.getPositionManager().locationsOfLine(classType, getSourcePosition());
142       if (!locations.isEmpty()) {
143         for (Location loc : locations) {
144           if (LOG.isDebugEnabled()) {
145             LOG.debug("Found location [codeIndex=" + loc.codeIndex() +"] for reference type " + classType.name() + " at line " + getLineIndex() + "; isObsolete: " + (debugProcess.getVirtualMachineProxy().versionHigher("1.4") && loc.method().isObsolete()));
146           }
147           if (!acceptLocation(debugProcess, classType, loc)) {
148             continue;
149           }
150           final BreakpointRequest request = debugProcess.getRequestsManager().createBreakpointRequest(this, loc);
151           debugProcess.getRequestsManager().enableRequest(request);
152           if (LOG.isDebugEnabled()) {
153             LOG.debug("Created breakpoint request for reference type " + classType.name() + " at line " + getLineIndex() + "; codeIndex=" + loc.codeIndex());
154           }
155         }
156       }
157       else {
158         // there's no executable code in this class
159         debugProcess.getRequestsManager().setInvalid(this, DebuggerBundle.message(
160           "error.invalid.breakpoint.no.executable.code", (getLineIndex() + 1), classType.name())
161         );
162         if (LOG.isDebugEnabled()) {
163           LOG.debug("No locations of type " + classType.name() + " found at line " + getLineIndex());
164         }
165       }
166     }
167     catch (ClassNotPreparedException ex) {
168       if (LOG.isDebugEnabled()) {
169         LOG.debug("ClassNotPreparedException: " + ex.getMessage());
170       }
171       // there's a chance to add a breakpoint when the class is prepared
172     }
173     catch (ObjectCollectedException ex) {
174       if (LOG.isDebugEnabled()) {
175         LOG.debug("ObjectCollectedException: " + ex.getMessage());
176       }
177       // there's a chance to add a breakpoint when the class is prepared
178     }
179     catch (InvalidLineNumberException ex) {
180       if (LOG.isDebugEnabled()) {
181         LOG.debug("InvalidLineNumberException: " + ex.getMessage());
182       }
183       debugProcess.getRequestsManager().setInvalid(this, DebuggerBundle.message("error.invalid.breakpoint.bad.line.number"));
184     }
185     catch (InternalException ex) {
186       LOG.info(ex);
187     }
188     catch(Exception ex) {
189       LOG.info(ex);
190     }
191     updateUI();
192   }
193
194   private static Pattern ourAnonymousPattern = Pattern.compile(".*\\$\\d*$");
195   private static boolean isAnonymousClass(ReferenceType classType) {
196     if (classType instanceof ClassType) {
197       return ourAnonymousPattern.matcher(classType.name()).matches();
198     }
199     return false;
200   }
201
202   protected boolean acceptLocation(final DebugProcessImpl debugProcess, ReferenceType classType, final Location loc) {
203     Method method = loc.method();
204     if (DebuggerUtils.isSynthetic(method)) {
205       return false;
206     }
207     boolean res = !(method.isConstructor() && loc.codeIndex() == 0 && isAnonymousClass(classType));
208     if (!res) return false;
209     return ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
210       @Override
211       public Boolean compute() {
212         if (getProperties() instanceof JavaLineBreakpointProperties) {
213           Integer ordinal = ((JavaLineBreakpointProperties)getProperties()).getLambdaOrdinal();
214           if (ordinal == null) return true;
215           PsiElement containingMethod = getContainingMethod();
216           if (containingMethod == null) return false;
217           SourcePosition position = debugProcess.getPositionManager().getSourcePosition(loc);
218           if (position == null) return false;
219           return DebuggerUtilsEx.inTheMethod(position, containingMethod);
220         }
221         return true;
222       }
223     });
224   }
225
226   @Nullable
227   public PsiElement getContainingMethod() {
228     SourcePosition position = getSourcePosition();
229     if (position == null) return null;
230     if (getProperties() instanceof JavaLineBreakpointProperties) {
231       Integer ordinal = ((JavaLineBreakpointProperties)getProperties()).getLambdaOrdinal();
232       if (ordinal > -1) {
233         List<PsiLambdaExpression> lambdas = DebuggerUtilsEx.collectLambdas(position, true);
234         if (ordinal < lambdas.size()) {
235           return lambdas.get(ordinal);
236         }
237       }
238     }
239     return DebuggerUtilsEx.getContainingMethod(position);
240   }
241
242   private boolean isInScopeOf(DebugProcessImpl debugProcess, String className) {
243     final SourcePosition position = getSourcePosition();
244     if (position != null) {
245       final VirtualFile breakpointFile = position.getFile().getVirtualFile();
246       final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
247       if (breakpointFile != null && fileIndex.isUnderSourceRootOfType(breakpointFile, JavaModuleSourceRootTypes.SOURCES)) {
248         if (debugProcess.getSearchScope().contains(breakpointFile)) {
249           return true;
250         }
251         // apply filtering to breakpoints from content sources only, not for sources attached to libraries
252         final Collection<VirtualFile> candidates = findClassCandidatesInSourceContent(className, debugProcess.getSearchScope(), fileIndex);
253         if (LOG.isDebugEnabled()) {
254           LOG.debug("Found "+ (candidates == null? "null" : candidates.size()) + " candidate containing files for class " + className);
255         }
256         if (candidates == null) {
257           // If no candidates are found in scope then assume that class is loaded dynamically and allow breakpoint
258           return true;
259         }
260
261         // breakpointFile is not in scope here and there are some candidates in scope
262         //for (VirtualFile classFile : candidates) {
263         //  if (LOG.isDebugEnabled()) {
264         //    LOG.debug("Breakpoint file: " + breakpointFile.getPath()+ "; candidate file: " + classFile.getPath());
265         //  }
266         //  if (breakpointFile.equals(classFile)) {
267         //    return true;
268         //  }
269         //}
270         if (LOG.isDebugEnabled()) {
271           final GlobalSearchScope scope = debugProcess.getSearchScope();
272           final boolean contains = scope.contains(breakpointFile);
273           final Project project = getProject();
274           final List<VirtualFile> files = ContainerUtil.map(
275             JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, scope), new Function<PsiClass, VirtualFile>() {
276             @Override
277             public VirtualFile fun(PsiClass aClass) {
278               return aClass.getContainingFile().getVirtualFile();
279             }
280           });
281           final List<VirtualFile> allFiles = ContainerUtil.map(
282             JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, new EverythingGlobalScope(project)), new Function<PsiClass, VirtualFile>() {
283             @Override
284             public VirtualFile fun(PsiClass aClass) {
285               return aClass.getContainingFile().getVirtualFile();
286             }
287           });
288           final VirtualFile contentRoot = fileIndex.getContentRootForFile(breakpointFile);
289           final Module module = fileIndex.getModuleForFile(breakpointFile);
290
291           LOG.debug("Did not find '" +
292                     className + "' in " + scope +
293                     "; contains=" + contains +
294                     "; contentRoot=" + contentRoot +
295                     "; module = " + module +
296                     "; all files in index are: " + files+
297                     "; all possible files are: " + allFiles
298           );
299         }
300         
301         return false;
302       }
303     }
304     return true;
305   }
306
307   @Nullable
308   private Collection<VirtualFile> findClassCandidatesInSourceContent(final String className, final GlobalSearchScope scope, final ProjectFileIndex fileIndex) {
309     final int dollarIndex = className.indexOf("$");
310     final String topLevelClassName = dollarIndex >= 0? className.substring(0, dollarIndex) : className;
311     return ApplicationManager.getApplication().runReadAction(new Computable<Collection<VirtualFile>>() {
312       @Override
313       @Nullable
314       public Collection<VirtualFile> compute() {
315         final PsiClass[] classes = JavaPsiFacade.getInstance(myProject).findClasses(topLevelClassName, scope);
316         if (LOG.isDebugEnabled()) {
317           LOG.debug("Found "+ classes.length + " classes " + topLevelClassName + " in scope "+scope);
318         }
319         if (classes.length == 0) {
320           return null;
321         }
322         final List<VirtualFile> list = new ArrayList<VirtualFile>(classes.length);
323         for (PsiClass aClass : classes) {
324           final PsiFile psiFile = aClass.getContainingFile();
325           
326           if (LOG.isDebugEnabled()) {
327             final StringBuilder msg = new StringBuilder();
328             msg.append("Checking class ").append(aClass.getQualifiedName());
329             msg.append("\n\t").append("PsiFile=").append(psiFile);
330             if (psiFile != null) {
331               final VirtualFile vFile = psiFile.getVirtualFile();
332               msg.append("\n\t").append("VirtualFile=").append(vFile);
333               if (vFile != null) {
334                 msg.append("\n\t").append("isInSourceContent=").append(fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES));
335               }
336             }
337             LOG.debug(msg.toString());
338           }
339           
340           if (psiFile == null) {
341             return null;
342           }
343           final VirtualFile vFile = psiFile.getVirtualFile();
344           if (vFile == null || !fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES)) {
345             return null; // this will switch off the check if at least one class is from libraries
346           }
347           list.add(vFile);
348         }
349         return list;
350       }
351     });
352   }
353
354   @Override
355   protected String calculateEventClass(EvaluationContextImpl context, LocatableEvent event) throws EvaluateException {
356     String className = null;
357     final ObjectReference thisObject = (ObjectReference)context.getThisObject();
358     if (thisObject != null) {
359       className = thisObject.referenceType().name();
360     }
361     else {
362       final StackFrameProxyImpl frame = context.getFrameProxy();
363       if (frame != null) {
364         className = frame.location().declaringType().name();
365       }
366     }
367     return className;
368   }
369
370   @Override
371   public String getShortName() {
372     return getDisplayInfoInternal(false, 30);
373   }
374
375   @Override
376   public String getDisplayName() {
377     return getDisplayInfoInternal(true, -1);
378   }
379
380   private String getDisplayInfoInternal(boolean showPackageInfo, int totalTextLength) {
381     if(isValid()) {
382       final int lineNumber = myXBreakpoint.getSourcePosition().getLine() + 1;
383       String className = getClassName();
384       final boolean hasClassInfo = className != null && className.length() > 0;
385       final String methodName = getMethodName();
386       final String displayName = methodName != null? methodName + "()" : null;
387       final boolean hasMethodInfo = displayName != null && displayName.length() > 0;
388       if (hasClassInfo || hasMethodInfo) {
389         final StringBuilder info = StringBuilderSpinAllocator.alloc();
390         try {
391           boolean isFile = myXBreakpoint.getSourcePosition().getFile().getName().equals(className);
392           String packageName = null;
393           if (hasClassInfo) {
394             final int dotIndex = className.lastIndexOf(".");
395             if (dotIndex >= 0 && !isFile) {
396               packageName = className.substring(0, dotIndex);
397               className = className.substring(dotIndex + 1); 
398             }
399
400             if (totalTextLength != -1) {
401               if (className.length() + (hasMethodInfo ? displayName.length() : 0) > totalTextLength + 3) {
402                 int offset = totalTextLength - (hasMethodInfo ? displayName.length() : 0);
403                 if (offset > 0 && offset < className.length()) {
404                   className = className.substring(className.length() - offset);
405                   info.append("...");
406                 }
407               }
408             }
409             
410             info.append(className);
411           }
412           if(hasMethodInfo) {
413             if (isFile) {
414               info.append(":");
415             }
416             else if (hasClassInfo) {
417               info.append(".");
418             }
419             info.append(displayName);
420           }
421           if (showPackageInfo && packageName != null) {
422             info.append(" (").append(packageName).append(")");
423           }
424           return DebuggerBundle.message("line.breakpoint.display.name.with.class.or.method", lineNumber, info.toString());
425         }
426         finally {
427           StringBuilderSpinAllocator.dispose(info);
428         }
429       }
430       return DebuggerBundle.message("line.breakpoint.display.name", lineNumber);
431     }
432     return DebuggerBundle.message("status.breakpoint.invalid");
433   }
434
435   @Nullable
436   private static String findOwnerMethod(final PsiFile file, final int offset) {
437     if (offset < 0 || file instanceof JspFile) {
438       return null;
439     }
440     if (file instanceof PsiClassOwner) {
441       return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
442         @Override
443         public String compute() {
444           final PsiMethod method = DebuggerUtilsEx.findPsiMethod(file, offset);
445           return method != null? method.getName() : null;
446         }
447       });
448     }
449     return null;
450   }
451
452   @Override
453   public String getEventMessage(LocatableEvent event) {
454     final Location location = event.location();
455     String sourceName;
456     try {
457       sourceName = location.sourceName();
458     }
459     catch (AbsentInformationException e) {
460       sourceName = getFileName();
461     }
462
463     final boolean printFullTrace = Registry.is("debugger.breakpoint.message.full.trace");
464
465     StringBuilder builder = new StringBuilder();
466     if (printFullTrace) {
467       builder.append(DebuggerBundle.message(
468         "status.line.breakpoint.reached.full.trace",
469         DebuggerUtilsEx.getLocationMethodQName(location))
470       );
471       try {
472         final List<StackFrame> frames = event.thread().frames();
473         renderTrace(frames, builder);
474       }
475       catch (IncompatibleThreadStateException e) {
476         builder.append("Stacktrace not available: ").append(e.getMessage());
477       }
478     }
479     else {
480       builder.append(DebuggerBundle.message(
481         "status.line.breakpoint.reached",
482         DebuggerUtilsEx.getLocationMethodQName(location),
483         sourceName,
484         getLineIndex() + 1
485       ));
486     }
487     return builder.toString();
488   }
489
490   private static void renderTrace(List<StackFrame> frames, StringBuilder buffer) {
491     for (final StackFrame stackFrame : frames) {
492       final Location location = stackFrame.location();
493       buffer.append("\n\t  ").append(ThreadDumpAction.renderLocation(location));
494     }
495   }
496
497   @Override
498   public PsiElement getEvaluationElement() {
499     return ContextUtil.getContextElement(getSourcePosition());
500   }
501
502   public static LineBreakpoint create(@NotNull Project project, XBreakpoint xBreakpoint) {
503     LineBreakpoint breakpoint = new LineBreakpoint(project, xBreakpoint);
504     return (LineBreakpoint)breakpoint.init();
505   }
506
507   //@Override
508   //public boolean canMoveTo(SourcePosition position) {
509   //  if (!super.canMoveTo(position)) {
510   //    return false;
511   //  }
512   //  final Document document = PsiDocumentManager.getInstance(getProject()).getDocument(position.getFile());
513   //  return canAddLineBreakpoint(myProject, document, position.getLine());
514   //}
515
516   public static boolean canAddLineBreakpoint(Project project, final Document document, final int lineIndex) {
517     if (lineIndex < 0 || lineIndex >= document.getLineCount()) {
518       return false;
519     }
520     final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(project).getBreakpointManager();
521     final LineBreakpoint breakpointAtLine = breakpointManager.findBreakpoint( document, document.getLineStartOffset(lineIndex), CATEGORY);
522     if (breakpointAtLine != null) {
523       // there already exists a line breakpoint at this line
524       return false;
525     }
526     PsiDocumentManager.getInstance(project).commitDocument(document);
527
528     final boolean[] canAdd = new boolean[]{false};
529     XDebuggerUtil.getInstance().iterateLine(project, document, lineIndex, new Processor<PsiElement>() {
530       @Override
531       public boolean process(PsiElement element) {
532         if ((element instanceof PsiWhiteSpace) || (PsiTreeUtil.getParentOfType(element, PsiComment.class, false) != null)) {
533           return true;
534         }
535         PsiElement child = element;
536         while(element != null) {
537
538           final int offset = element.getTextOffset();
539           if (offset >= 0) {
540             if (document.getLineNumber(offset) != lineIndex) {
541               break;
542             }
543           }
544           child = element;
545           element = element.getParent();
546         }
547
548         if(child instanceof PsiMethod && child.getTextRange().getEndOffset() >= document.getLineEndOffset(lineIndex)) {
549           PsiCodeBlock body = ((PsiMethod)child).getBody();
550           if(body == null) {
551             canAdd[0] = false;
552           }
553           else {
554             PsiStatement[] statements = body.getStatements();
555             canAdd[0] = statements.length > 0 && document.getLineNumber(statements[0].getTextOffset()) == lineIndex;
556           }
557         }
558         else {
559           canAdd[0] = true;
560         }
561         return false;
562       }
563     });
564
565     return canAdd[0];
566   }
567
568   @Nullable
569   public String getMethodName() {
570     XSourcePosition position = myXBreakpoint.getSourcePosition();
571     if (position != null) {
572       int offset = position.getOffset();
573       return findOwnerMethod(getPsiFile(), offset);
574     }
575     return null;
576   }
577 }