From 554aea463ac10309f9a756fb62c356c2e82b1de0 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Tue, 15 Jan 2019 11:44:55 +0700 Subject: [PATCH] IDEA-205404 When navigating from Java stacktrace try to find the relevant position within line --- .../execution/filters/ExceptionFilter.java | 25 +++- .../filters/ExceptionWorkerTest.java | 74 ++++++++++- .../execution/filters/ExceptionWorker.java | 124 +++++++++++++++++- .../filters/HyperlinkInfoFactory.java | 16 +++ .../impl/HyperlinkInfoFactoryImpl.java | 15 ++- .../impl/MultipleFilesHyperlinkInfo.java | 15 ++- 6 files changed, 258 insertions(+), 11 deletions(-) diff --git a/java/execution/openapi/src/com/intellij/execution/filters/ExceptionFilter.java b/java/execution/openapi/src/com/intellij/execution/filters/ExceptionFilter.java index 750d95d28268..23c1c15531ac 100644 --- a/java/execution/openapi/src/com/intellij/execution/filters/ExceptionFilter.java +++ b/java/execution/openapi/src/com/intellij/execution/filters/ExceptionFilter.java @@ -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; } } diff --git a/java/java-tests/testSrc/com/intellij/java/execution/filters/ExceptionWorkerTest.java b/java/java-tests/testSrc/com/intellij/java/execution/filters/ExceptionWorkerTest.java index 6671010fc088..2a0493eed78c 100644 --- a/java/java-tests/testSrc/com/intellij/java/execution/filters/ExceptionWorkerTest.java +++ b/java/java-tests/testSrc/com/intellij/java/execution/filters/ExceptionWorkerTest.java @@ -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> traceAndPositions = Arrays.asList( + Trinity.create("Exception in thread \"main\" java.lang.ArrayIndexOutOfBoundsException: 1", null, null), + Trinity.create("\tat SomeClass.(SomeClass.java:4)", 4, 36), + Trinity.create("\tat SomeClass$1.(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 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); + } + } + } } diff --git a/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java b/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java index 6cc764984b14..1a17dc3e63a0 100644 --- a/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java +++ b/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java @@ -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 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("")) { + 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 { + 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 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; + } + } } diff --git a/platform/lang-api/src/com/intellij/execution/filters/HyperlinkInfoFactory.java b/platform/lang-api/src/com/intellij/execution/filters/HyperlinkInfoFactory.java index 42beb03faeff..49bd699391a5 100644 --- a/platform/lang-api/src/com/intellij/execution/filters/HyperlinkInfoFactory.java +++ b/platform/lang-api/src/com/intellij/execution/filters/HyperlinkInfoFactory.java @@ -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 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 files, + int line, + @NotNull Project project, + ToIntFunction columnFinder); } diff --git a/platform/lang-impl/src/com/intellij/execution/filters/impl/HyperlinkInfoFactoryImpl.java b/platform/lang-impl/src/com/intellij/execution/filters/impl/HyperlinkInfoFactoryImpl.java index f8a800094410..2c9dc33f334f 100644 --- a/platform/lang-impl/src/com/intellij/execution/filters/impl/HyperlinkInfoFactoryImpl.java +++ b/platform/lang-impl/src/com/intellij/execution/filters/impl/HyperlinkInfoFactoryImpl.java @@ -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 files, + int line, + @NotNull Project project, + @Nullable ToIntFunction columnFinder) { + return new MultipleFilesHyperlinkInfo(files, line, project, columnFinder); + } } diff --git a/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java b/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java index e78a39c5dc78..740fc78e3b6d 100644 --- a/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java +++ b/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java @@ -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 myVirtualFiles; private final int myLineNumber; private final Project myProject; + private final ToIntFunction myColumnFinder; MultipleFilesHyperlinkInfo(@NotNull List virtualFiles, int lineNumber, @NotNull Project project) { + this(virtualFiles, lineNumber, project, null); + } + + MultipleFilesHyperlinkInfo(@NotNull List virtualFiles, + int lineNumber, + @NotNull Project project, + @Nullable ToIntFunction 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) {