IDEA-205404 When navigating from Java stacktrace try to find the relevant position within line

This commit is contained in:
Tagir Valeev
2019-01-15 11:47:40 +07:00
parent 424f56e89e
commit 554aea463a
6 changed files with 258 additions and 11 deletions
@@ -16,11 +16,20 @@
package com.intellij.execution.filters;
import com.intellij.openapi.project.DumbAware;
import com.intellij.psi.PsiArrayAccessExpression;
import com.intellij.psi.PsiJavaToken;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiElementFilter;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ExceptionFilter implements Filter, DumbAware {
private final ExceptionInfoCache myCache;
private PsiElementFilter myNextLineRefiner;
private static final Pattern EXCEPTION_PATTERN = Pattern.compile("^Exception in thread \".+\" java\\.lang\\.(\\w+):");
public ExceptionFilter(@NotNull final GlobalSearchScope scope) {
myCache = new ExceptionInfoCache(scope);
@@ -29,6 +38,20 @@ public class ExceptionFilter implements Filter, DumbAware {
@Override
public Result applyFilter(final String line, final int textEndOffset) {
ExceptionWorker worker = new ExceptionWorker(myCache);
return worker.execute(line, textEndOffset);
Result result = worker.execute(line, textEndOffset, myNextLineRefiner);
myNextLineRefiner = result == null ? getRefinerFromException(line) : worker.getLocationRefiner();
return result;
}
private static PsiElementFilter getRefinerFromException(String line) {
Matcher matcher = EXCEPTION_PATTERN.matcher(line);
if(!matcher.find()) return null;
String exceptionName = matcher.group(1);
if("ArrayIndexOutOfBoundsException".equals(exceptionName)) {
return element -> element instanceof PsiJavaToken &&
element.textMatches("[") &&
element.getParent() instanceof PsiArrayAccessExpression;
}
return null;
}
}
@@ -15,18 +15,21 @@
*/
package com.intellij.java.execution.filters;
import com.intellij.execution.filters.ExceptionExFilterFactory;
import com.intellij.execution.filters.ExceptionInfoCache;
import com.intellij.execution.filters.ExceptionWorker;
import com.intellij.execution.filters.FilterMixin;
import com.intellij.execution.filters.*;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.Trinity;
import com.intellij.psi.PsiClass;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
import org.intellij.lang.annotations.Language;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author gregsh
@@ -125,4 +128,67 @@ public class ExceptionWorkerTest extends LightCodeInsightFixtureTestCase {
assertNull(ExceptionWorker.parseExceptionLine(line));
assertNull(ExceptionWorker.parseExceptionLine(line + "\n"));
}
public void testColumnFinder() {
@Language("JAVA") String classText =
"/** @noinspection ALL*/\n" +
"public class SomeClass {\n" +
" SomeClass() {\n" +
" System.out.println((new int[0])[1]);\n" +
" }\n" +
" static class Inner implements Runnable {\n" +
" int test = 4;\n" +
" public void run() {\n" +
" System.out.println(test + test() + SomeClass.test());\n" +
" }\n" +
" int test() { return 0; }\n" +
" }\n" +
" private static int test() {\n" +
" new SomeClass() {};\n" +
" return 1;\n" +
" }\n" +
" public static void main(String[] args) {\n" +
" class X implements Runnable {\n" +
" public void run() {\n" +
" new Runnable() {\n" +
" public void run() {\n" +
" new Inner().run();this.run();\n" +
" }\n" +
" }.run();\n" +
" }\n" +
" }\n" +
" new X().run();\n" +
" }\n" +
"}";
myFixture.configureByText("SomeClass.java", classText);
Editor editor = myFixture.getEditor();
assertEquals(classText, editor.getDocument().getText());
List<Trinity<String, Integer, Integer>> traceAndPositions = Arrays.asList(
Trinity.create("Exception in thread \"main\" java.lang.ArrayIndexOutOfBoundsException: 1", null, null),
Trinity.create("\tat SomeClass.<init>(SomeClass.java:4)", 4, 36),
Trinity.create("\tat SomeClass$1.<init>(SomeClass.java:14)", 14, 9),
Trinity.create("\tat SomeClass.test(SomeClass.java:14)", 14, 9),
Trinity.create("\tat SomeClass.access$000(SomeClass.java:2)", 2, 1),
Trinity.create("\tat SomeClass$Inner.run(SomeClass.java:9)", 9, 52),
Trinity.create("\tat SomeClass$1X$1.run(SomeClass.java:22)", 22, 25),
Trinity.create("\tat SomeClass$1X.run(SomeClass.java:24)", 24, 11),
Trinity.create("\tat SomeClass.main(SomeClass.java:27)", 27, 13));
ExceptionFilter filter = new ExceptionFilter(myFixture.getFile().getResolveScope());
for (Trinity<String, Integer, Integer> line : traceAndPositions) {
String stackLine = line.getFirst();
Filter.Result result = filter.applyFilter(stackLine, stackLine.length());
Integer row = line.getSecond();
Integer column = line.getThird();
if (row == null) {
assertNull(result);
}
else {
HyperlinkInfo info = result.getFirstHyperlinkInfo();
assertNotNull(info);
info.navigate(getProject());
LogicalPosition actualPos = editor.getCaretModel().getLogicalPosition();
assertEquals(new LogicalPosition(row - 1, column - 1), actualPos);
}
}
}
}
@@ -15,18 +15,23 @@
*/
package com.intellij.execution.filters;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.colors.CodeInsightColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiShortNamesCache;
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiElementFilter;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
@@ -36,6 +41,7 @@ import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.ToIntFunction;
public class ExceptionWorker {
@NonNls private static final String AT = "at";
@@ -49,6 +55,7 @@ public class ExceptionWorker {
private String myMethod;
private ParsedLine myInfo;
private final ExceptionInfoCache myCache;
private PsiElementFilter myLocationRefiner;
public ExceptionWorker(@NotNull ExceptionInfoCache cache) {
myProject = cache.getProject();
@@ -56,6 +63,10 @@ public class ExceptionWorker {
}
public Filter.Result execute(final String line, final int textEndOffset) {
return execute(line, textEndOffset, null);
}
public Filter.Result execute(final String line, final int textEndOffset, PsiElementFilter elementMatcher) {
myResult = null;
myInfo = parseExceptionLine(line);
if (myInfo == null) {
@@ -113,12 +124,23 @@ public class ExceptionWorker {
else {
virtualFiles = virtualFilesInContent;
}
HyperlinkInfo linkInfo = HyperlinkInfoFactory.getInstance().createMultipleFilesHyperlinkInfo(virtualFiles, myInfo.lineNumber - 1, myProject);
ToIntFunction<PsiFile> columnFinder = elementMatcher == null ? null : new ExceptionColumnFinder(elementMatcher, myInfo.lineNumber - 1);
HyperlinkInfo linkInfo =
HyperlinkInfoFactory.getInstance().createMultipleFilesHyperlinkInfo(virtualFiles, myInfo.lineNumber - 1, myProject, columnFinder);
Filter.Result result = new Filter.Result(highlightStartOffset, highlightEndOffset, linkInfo, attributes);
if (myMethod.startsWith("access$")) {
myLocationRefiner = elementMatcher;
} else {
myLocationRefiner = new StackFrameMatcher(line, myInfo);
}
myResult = result;
return result;
}
public PsiElementFilter getLocationRefiner() {
return myLocationRefiner;
}
private static int getLineNumber(String lineString) {
// some quick checks to avoid costly exceptions
if (lineString.isEmpty() || lineString.length() > 9 || !Character.isDigit(lineString.charAt(0))) {
@@ -305,4 +327,100 @@ public class ExceptionWorker {
return new ParsedLine(classFqnRange, methodNameRange, fileLineRange, fileAndLine.substring(0, colonIndex).trim(), lineNumber);
}
}
private static class StackFrameMatcher implements PsiElementFilter {
private final String myMethodName;
private final String myClassName;
private final String mySimpleName;
private final boolean myHasDollarInName;
private StackFrameMatcher(@NotNull String line, @NotNull ParsedLine info) {
myMethodName = info.methodNameRange.substring(line);
myClassName = info.classFqnRange.substring(line);
mySimpleName = StringUtil.getShortName(myClassName);
myHasDollarInName = mySimpleName.contains("$");
}
@Override
public boolean isAccepted(PsiElement element) {
if (!(element instanceof PsiIdentifier)) return false;
if (myMethodName.equals("<init>")) {
if (myHasDollarInName || element.textMatches(StringUtil.getShortName(myClassName))) {
PsiElement parent = element.getParent();
while (parent instanceof PsiJavaCodeReferenceElement) {
parent = parent.getParent();
}
if (parent instanceof PsiAnonymousClass) {
return isTargetClass(parent) || isTargetClass(((PsiAnonymousClass)parent).getSuperClass());
}
if (parent instanceof PsiNewExpression) {
PsiJavaCodeReferenceElement ref = ((PsiNewExpression)parent).getClassOrAnonymousClassReference();
if (ref != null && isTargetClass(ref.resolve())) {
return true;
}
}
}
}
else if (element.textMatches(myMethodName)) {
PsiElement parent = element.getParent();
if (parent instanceof PsiReferenceExpression) {
PsiElement target = ((PsiReferenceExpression)parent).resolve();
if (target instanceof PsiMethod && isTargetClass(((PsiMethod)target).getContainingClass())) {
return true;
}
}
}
return false;
}
private boolean isTargetClass(PsiElement psiClass) {
if (!(psiClass instanceof PsiClass)) return false;
return isTargetClass((PsiClass)psiClass, myClassName);
}
private static boolean isTargetClass(PsiClass psiClass, String className) {
if (psiClass == null) return false;
String name = psiClass.getQualifiedName();
if (className.equals(name)) return true;
if (StringUtil.getShortName(className).contains("$")) {
String prefix = StringUtil.substringBeforeLast(className, "$");
String suffix = StringUtil.substringAfterLast(className, "$");
PsiClass containingClass = PsiTreeUtil.getParentOfType(psiClass, PsiClass.class, true);
return suffix != null && isTargetClass(containingClass, prefix) &&
ClassUtil.findNonQualifiedClassByIndex(suffix, containingClass, true) == psiClass;
}
return false;
}
}
private static class ExceptionColumnFinder implements ToIntFunction<PsiFile> {
private final PsiElementFilter myElementMatcher;
private final int myLineNumber;
private ExceptionColumnFinder(@NotNull PsiElementFilter elementMatcher, int lineNumber) {
myElementMatcher = elementMatcher;
myLineNumber = lineNumber;
}
@Override
public int applyAsInt(PsiFile file) {
Document document = FileDocumentManager.getInstance().getDocument(file.getVirtualFile());
if (document == null || document.getLineCount() <= myLineNumber) return 0;
int startOffset = document.getLineStartOffset(myLineNumber);
int endOffset = document.getLineEndOffset(myLineNumber);
PsiElement element = file.findElementAt(startOffset);
List<PsiElement> candidates = new ArrayList<>();
while (element != null && element.getTextRange().getStartOffset() < endOffset) {
if (myElementMatcher.isAccepted(element)) {
candidates.add(element);
if (candidates.size() > 1) return 0;
}
element = PsiTreeUtil.nextLeaf(element);
}
if (candidates.size() == 1) {
return candidates.get(0).getTextRange().getStartOffset() - startOffset;
}
return 0;
}
}
}
@@ -18,9 +18,11 @@ package com.intellij.execution.filters;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.function.ToIntFunction;
/**
* @author nik
@@ -34,4 +36,18 @@ public abstract class HyperlinkInfoFactory {
@NotNull
public abstract HyperlinkInfo createMultipleFilesHyperlinkInfo(@NotNull List<VirtualFile> files,
int line, @NotNull Project project);
/**
* Creates a hyperlink which points to several files with ability to calculate a position inside line
* @param files list of files to navigate to (will be suggested to user)
* @param line line number to navigate to
* @param project a project
* @param columnFinder a function which accepts a selected file and returns a column within the specified line to navigate to
* @return newly created HyperlinkInfo which navigates to given line and column
*/
@NotNull
public abstract HyperlinkInfo createMultipleFilesHyperlinkInfo(@NotNull List<VirtualFile> files,
int line,
@NotNull Project project,
ToIntFunction<? super PsiFile> columnFinder);
}
@@ -15,12 +15,16 @@
*/
package com.intellij.execution.filters.impl;
import com.intellij.execution.filters.*;
import com.intellij.execution.filters.HyperlinkInfo;
import com.intellij.execution.filters.HyperlinkInfoFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.ToIntFunction;
/**
* @author nik
@@ -33,4 +37,13 @@ public class HyperlinkInfoFactoryImpl extends HyperlinkInfoFactory {
int line, @NotNull Project project) {
return new MultipleFilesHyperlinkInfo(files, line, project);
}
@NotNull
@Override
public HyperlinkInfo createMultipleFilesHyperlinkInfo(@NotNull List<VirtualFile> files,
int line,
@NotNull Project project,
@Nullable ToIntFunction<? super PsiFile> columnFinder) {
return new MultipleFilesHyperlinkInfo(files, line, project, columnFinder);
}
}
@@ -37,6 +37,7 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.ToIntFunction;
/**
* @author nik
@@ -45,11 +46,20 @@ class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperl
private final List<? extends VirtualFile> myVirtualFiles;
private final int myLineNumber;
private final Project myProject;
private final ToIntFunction<? super PsiFile> myColumnFinder;
MultipleFilesHyperlinkInfo(@NotNull List<? extends VirtualFile> virtualFiles, int lineNumber, @NotNull Project project) {
this(virtualFiles, lineNumber, project, null);
}
MultipleFilesHyperlinkInfo(@NotNull List<? extends VirtualFile> virtualFiles,
int lineNumber,
@NotNull Project project,
@Nullable ToIntFunction<? super PsiFile> columnFinder) {
myVirtualFiles = virtualFiles;
myLineNumber = lineNumber;
myProject = project;
myColumnFinder = columnFinder == null ? f -> 0 : columnFinder;
}
@Override
@@ -75,7 +85,8 @@ class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperl
if (currentFiles.isEmpty()) return;
if (currentFiles.size() == 1) {
new OpenFileHyperlinkInfo(myProject, currentFiles.get(0).getVirtualFile(), myLineNumber).navigate(project);
PsiFile file = currentFiles.get(0);
new OpenFileHyperlinkInfo(myProject, file.getVirtualFile(), myLineNumber, myColumnFinder.applyAsInt(file)).navigate(project);
}
else {
JFrame frame = WindowManager.getInstance().getFrame(project);
@@ -86,7 +97,7 @@ class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperl
.setTitle("Choose Target File")
.setItemChosenCallback((selectedValue) -> {
VirtualFile file = selectedValue.getVirtualFile();
new OpenFileHyperlinkInfo(myProject, file, myLineNumber).navigate(project);
new OpenFileHyperlinkInfo(myProject, file, myLineNumber, myColumnFinder.applyAsInt(selectedValue)).navigate(project);
})
.createPopup();
if (hyperlinkLocationPoint != null) {