From 90877751c9cef3df3c35e19b64f354366d7734f3 Mon Sep 17 00:00:00 2001 From: peter Date: Wed, 2 May 2018 17:11:13 +0200 Subject: [PATCH] IDEA-186244 Analyse Stack Trace does not understand jstack -F traces --- .../filters/ExceptionExFilterFactory.java | 18 +- .../VcsContentAnnotationExceptionFilter.java | 8 +- .../intellij/unscramble/ThreadDumpParser.java | 8 +- .../filters/ExceptionWorkerTest.java | 33 ++-- .../unscramble/ThreadDumpParserTest.groovy | 40 +++++ .../execution/filters/ExceptionWorker.java | 156 +++++++++++++----- 6 files changed, 191 insertions(+), 72 deletions(-) diff --git a/java/execution/openapi/src/com/intellij/execution/filters/ExceptionExFilterFactory.java b/java/execution/openapi/src/com/intellij/execution/filters/ExceptionExFilterFactory.java index 8532b9a62d81..0bdb4809749c 100644 --- a/java/execution/openapi/src/com/intellij/execution/filters/ExceptionExFilterFactory.java +++ b/java/execution/openapi/src/com/intellij/execution/filters/ExceptionExFilterFactory.java @@ -21,7 +21,6 @@ import com.intellij.openapi.editor.markup.EffectType; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.Trinity; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; @@ -65,8 +64,8 @@ public class ExceptionExFilterFactory implements ExceptionFilterFactory { final int startOffset, int startLineNumber, @NotNull final Consumer consumer) { - Map> visited = new THashMap<>(); - final Trinity emptyInfo = Trinity.create(null, null, null); + Map visited = new THashMap<>(); + ExceptionWorker.ParsedLine emptyInfo = new ExceptionWorker.ParsedLine(TextRange.EMPTY_RANGE, TextRange.EMPTY_RANGE, TextRange.EMPTY_RANGE, null, -1); final ExceptionWorker worker = new ExceptionWorker(myCache); for (int i = 0; i < copiedFragment.getLineCount(); i++) { @@ -75,11 +74,11 @@ public class ExceptionExFilterFactory implements ExceptionFilterFactory { String lineText = copiedFragment.getText(new TextRange(lineStartOffset, lineEndOffset)); if (!lineText.contains(".java:")) continue; - Trinity info = visited.get(lineText); + ExceptionWorker.ParsedLine info = visited.get(lineText); if (info == emptyInfo) continue; if (info == null) { - info = ReadAction.compute(() -> doparse(emptyInfo, worker, lineEndOffset, lineText)); + info = ReadAction.compute(() -> doParse(worker, lineEndOffset, lineText)); visited.put(lineText, info == null ? emptyInfo : info); if (info == null) { continue; @@ -87,7 +86,7 @@ public class ExceptionExFilterFactory implements ExceptionFilterFactory { } int off = startOffset + lineStartOffset; final Color color = UIUtil.getInactiveTextColor(); - consumer.consume(new AdditionalHighlight(off + info.first.getStartOffset(), off + info.second.getEndOffset()) { + consumer.consume(new AdditionalHighlight(off + info.classFqnRange.getStartOffset(), off + info.methodNameRange.getEndOffset()) { @NotNull @Override public TextAttributes getTextAttributes(@Nullable TextAttributes source) { @@ -97,9 +96,7 @@ public class ExceptionExFilterFactory implements ExceptionFilterFactory { } } - private Trinity doparse(Trinity emptyInfo, - ExceptionWorker worker, - int lineEndOffset, String lineText) { + private static ExceptionWorker.ParsedLine doParse(ExceptionWorker worker, int lineEndOffset, String lineText) { Result result = worker.execute(lineText, lineEndOffset); if (result == null) return null; HyperlinkInfo hyperlinkInfo = result.getHyperlinkInfo(); @@ -117,8 +114,7 @@ public class ExceptionExFilterFactory implements ExceptionFilterFactory { PsiTryStatement parent = PsiTreeUtil.getParentOfType(element, PsiTryStatement.class, true, PsiClass.class); PsiCodeBlock tryBlock = parent != null? parent.getTryBlock() : null; if (tryBlock == null || !tryBlock.getTextRange().contains(offset)) return null; - Trinity info = worker.getInfo(); - return info; + return worker.getInfo(); } @NotNull diff --git a/java/java-impl/src/com/intellij/openapi/vcs/contentAnnotation/VcsContentAnnotationExceptionFilter.java b/java/java-impl/src/com/intellij/openapi/vcs/contentAnnotation/VcsContentAnnotationExceptionFilter.java index 9f7d584b5c46..f2014071b63f 100644 --- a/java/java-impl/src/com/intellij/openapi/vcs/contentAnnotation/VcsContentAnnotationExceptionFilter.java +++ b/java/java-impl/src/com/intellij/openapi/vcs/contentAnnotation/VcsContentAnnotationExceptionFilter.java @@ -127,9 +127,9 @@ public class VcsContentAnnotationExceptionFilter implements Filter, FilterMixin final Document document = getDocumentForFile(worker); if (document == null) return; - int startFileOffset = worker.getInfo().getThird().getStartOffset(); + int startFileOffset = worker.getInfo().fileLineRange.getStartOffset(); int idx = lineText.indexOf(':', startFileOffset); - int endIdx = idx == -1 ? worker.getInfo().getThird().getEndOffset() : idx; + int endIdx = idx == -1 ? worker.getInfo().fileLineRange.getEndOffset() : idx; consumer.consume(new MyAdditionalHighlight(startOffset + lineStartOffset + startFileOffset + 1, startOffset + lineStartOffset + endIdx)); if (worker.getPsiClass() != null) { @@ -149,8 +149,8 @@ public class VcsContentAnnotationExceptionFilter implements Filter, FilterMixin } } if (methodChanged) { - consumer.consume(new MyAdditionalHighlight(startOffset + lineStartOffset + worker.getInfo().getSecond().getStartOffset(), - startOffset + lineStartOffset + worker.getInfo().getSecond().getEndOffset())); + consumer.consume(new MyAdditionalHighlight(startOffset + lineStartOffset + worker.getInfo().methodNameRange.getStartOffset(), + startOffset + lineStartOffset + worker.getInfo().methodNameRange.getEndOffset())); } } } diff --git a/java/java-impl/src/com/intellij/unscramble/ThreadDumpParser.java b/java/java-impl/src/com/intellij/unscramble/ThreadDumpParser.java index 32b5a81c5f6b..c71bf4a9028e 100644 --- a/java/java-impl/src/com/intellij/unscramble/ThreadDumpParser.java +++ b/java/java-impl/src/com/intellij/unscramble/ThreadDumpParser.java @@ -30,6 +30,7 @@ import java.util.regex.Pattern; */ public class ThreadDumpParser { private static final Pattern ourThreadStartPattern = Pattern.compile("^\\s*\"(.+)\".+(prio=\\d+ (?:os_prio=[^\\s]+ )?tid=[^\\s]+ nid=[^\\s]+|[Ii][Dd]=\\d+) ([^\\[]+)"); + private static final Pattern ourForcedThreadStartPattern = Pattern.compile("^\\s*Thread (\\d+): \\(state = (.+)\\)"); private static final Pattern ourYourkitThreadStartPattern = Pattern.compile("(?:\\s)*(.+) \\[([A-Z_, ]*)]"); private static final Pattern ourYourkitThreadStartPattern2 = Pattern.compile("(.+) State: (.+) CPU usage on sample: .+"); private static final Pattern ourThreadStatePattern = Pattern.compile("java\\.lang\\.Thread\\.State: (.+) \\((.+)\\)"); @@ -198,7 +199,12 @@ public class ThreadDumpParser { } return state; } - + + m = ourForcedThreadStartPattern.matcher(line); + if (m.matches()) { + return new ThreadState(m.group(1), m.group(2)); + } + boolean daemon = line.contains(" [DAEMON]"); if (daemon) { line = StringUtil.replace(line, " [DAEMON]", ""); 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 f2eb9d04f19f..2f657c6cf335 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 @@ -21,7 +21,6 @@ import com.intellij.execution.filters.FilterMixin; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.Trinity; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; @@ -79,29 +78,29 @@ public class ExceptionWorkerTest extends LightCodeInsightFixtureTestCase { } public void testAnomalyParenthesisParsing() { - String[][] data = new String[][]{ - {"at youtrack.jetbrains.com.Issue.IDEA_125137()(FooTest.groovy:2)\n", "youtrack.jetbrains.com.Issue", "IDEA_125137()", - "FooTest.groovy:2"}, - {"at youtrack.jetbrains.com.Issue.IDEA_125137()Hmm(FooTest.groovy:2)\n", "youtrack.jetbrains.com.Issue", "IDEA_125137()Hmm", - "FooTest.groovy:2"}, - {"p1.Cl.mee(p1.Cl.java:87) (A MESSAGE) IDEA-133794 (BUG START WITH 1)\n", "p1.Cl", "mee", "p1.Cl.java:87"} - }; - for (String[] datum : data) { - assertParsed(datum[0], datum[1], datum[2], datum[3]); - } + assertParsed("at youtrack.jetbrains.com.Issue.IDEA_125137()(FooTest.groovy:2)\n", "youtrack.jetbrains.com.Issue", "IDEA_125137()", "FooTest.groovy", 2); + assertParsed("at youtrack.jetbrains.com.Issue.IDEA_125137()Hmm(FooTest.groovy:2)\n", "youtrack.jetbrains.com.Issue", "IDEA_125137()Hmm", "FooTest.groovy", 2); + assertParsed("p1.Cl.mee(p1.Cl.java:87) (A MESSAGE) IDEA-133794 (BUG START WITH 1)\n", "p1.Cl", "mee", "p1.Cl.java", 87); } - private static void assertParsed(String line, String className, String methodName, String fileLine) { + private static void assertParsed(String line, String className, String methodName, String fileName, int lineIndex) { assertTrue(line.endsWith("\n")); - Trinity trinity = ExceptionWorker.parseExceptionLine(line); + ExceptionWorker.ParsedLine trinity = ExceptionWorker.parseExceptionLine(line); assertNotNull(trinity); - assertEquals(className, trinity.first.subSequence(line)); - assertEquals(methodName, trinity.second.subSequence(line)); - assertEquals(fileLine, trinity.third.subSequence(line)); + assertEquals(className, trinity.classFqnRange.subSequence(line)); + assertEquals(methodName, trinity.methodNameRange.subSequence(line)); + assertEquals(fileName, trinity.fileName); + assertEquals(lineIndex, trinity.lineNumber); } public void testYourKitFormat() { assertParsed("com.intellij.util.concurrency.Semaphore.waitFor(long) Semaphore.java:89\n", - "com.intellij.util.concurrency.Semaphore", "waitFor", "Semaphore.java:89"); + "com.intellij.util.concurrency.Semaphore", "waitFor", "Semaphore.java", 89); + } + + public void testForcedJstackFormat() { + assertParsed(" - java.lang.ref.ReferenceQueue.remove(long) @bci=151, line=143 (Compiled frame)\n", + "java.lang.ref.ReferenceQueue", "remove", null, 143); + } } diff --git a/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpParserTest.groovy b/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpParserTest.groovy index 68d25d165bff..be758454a33e 100644 --- a/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpParserTest.groovy +++ b/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpParserTest.groovy @@ -228,4 +228,44 @@ com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(Runnable) A 'AWT-EventQueue-0 2017.3#IC-173.SNAPSHOT IDEA, eap:true, os:Linux 3.13.0-117-generic, java-version:JetBrains s.r.o 1.8.0_152-release-867-b1'] } + void "test jstack -F format"() { + String text = ''' +Attaching to process ID 7370, please wait... +Debugger attached successfully. +Server compiler detected. +JVM version is 25.161-b12 +Deadlock Detection: + +No deadlocks found. + +Thread 8393: (state = BLOCKED) + - sun.misc.Unsafe.park(boolean, long) @bci=0 (Compiled frame; information may be imprecise) + - java.util.concurrent.locks.LockSupport.parkNanos(java.lang.Object, long) @bci=63, line=215 (Compiled frame) + - java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(java.util.concurrent.SynchronousQueue$TransferStack$SNode, boolean, long) @bci=283, line=460 (Compiled frame) + - java.util.concurrent.SynchronousQueue$TransferStack.transfer(java.lang.Object, boolean, long) @bci=175, line=362 (Compiled frame) + - java.util.concurrent.SynchronousQueue.poll(long, java.util.concurrent.TimeUnit) @bci=49, line=941 (Compiled frame) + - java.util.concurrent.ThreadPoolExecutor.getTask() @bci=247, line=1073 (Compiled frame) + - java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=74, line=1134 (Interpreted frame) + - java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=28, line=624 (Interpreted frame) + - java.lang.Thread.run() @bci=34, line=748 (Interpreted frame) + + +Thread 7399: (state = IN_NATIVE) + - sun.awt.X11.XToolkit.$$YJP$$waitForEvents(long) @bci=0 (Compiled frame; information may be imprecise) + - sun.awt.X11.XToolkit.waitForEvents(long) @bci=14 (Compiled frame) + - sun.awt.X11.XToolkit.run(boolean) @bci=298, line=568 (Interpreted frame) + - sun.awt.X11.XToolkit.run() @bci=38, line=532 (Interpreted frame) + - java.lang.Thread.run() @bci=34, line=748 (Interpreted frame) + + +Thread 7381: (state = BLOCKED) +''' + def threads = ThreadDumpParser.parse(text) + assert threads.collect { it.name } == ['8393', '7399', '7381'] + assert threads[0].stackTrace.contains('ThreadPoolExecutor') + assert threads[1].stackTrace.contains('XToolkit') + assert threads[2].emptyStackTrace + + } + } diff --git a/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java b/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java index 650e3dc95191..6cc764984b14 100644 --- a/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java +++ b/java/openapi/src/com/intellij/execution/filters/ExceptionWorker.java @@ -23,7 +23,6 @@ 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.Trinity; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiFile; @@ -48,7 +47,7 @@ public class ExceptionWorker { private PsiClass[] myClasses = PsiClass.EMPTY_ARRAY; private PsiFile[] myFiles = PsiFile.EMPTY_ARRAY; private String myMethod; - private Trinity myInfo; + private ParsedLine myInfo; private final ExceptionInfoCache myCache; public ExceptionWorker(@NotNull ExceptionInfoCache cache) { @@ -63,23 +62,15 @@ public class ExceptionWorker { return null; } - myMethod = myInfo.getSecond().substring(line); + myMethod = myInfo.methodNameRange.substring(line); - final String fileAndLine = myInfo.third.substring(line).trim(); - - final int colonIndex = fileAndLine.lastIndexOf(':'); - if (colonIndex < 0) return null; - - final int lineNumber = getLineNumber(fileAndLine.substring(colonIndex + 1)); - if (lineNumber < 0) return null; - - Pair pair = myCache.resolveClass(myInfo.first.substring(line).trim()); + Pair pair = myCache.resolveClass(myInfo.classFqnRange.substring(line).trim()); myClasses = pair.first; myFiles = pair.second; - if (myFiles.length == 0) { + if (myFiles.length == 0 && myInfo.fileName != null) { // try find the file with the required name //todo[nik] it would be better to use FilenameIndex here to honor the scope by it isn't accessible in Open API - myFiles = PsiShortNamesCache.getInstance(myProject).getFilesByName(fileAndLine.substring(0, colonIndex).trim()); + myFiles = PsiShortNamesCache.getInstance(myProject).getFilesByName(myInfo.fileName); } if (myFiles.length == 0) return null; @@ -93,8 +84,8 @@ public class ExceptionWorker { final int textStartOffset = textEndOffset - line.length(); - final int highlightStartOffset = textStartOffset + myInfo.third.getStartOffset(); - final int highlightEndOffset = textStartOffset + myInfo.third.getEndOffset(); + int highlightStartOffset = textStartOffset + myInfo.fileLineRange.getStartOffset(); + int highlightEndOffset = textStartOffset + myInfo.fileLineRange.getEndOffset(); ProjectFileIndex index = ProjectRootManager.getInstance(myProject).getFileIndex(); List virtualFilesInLibraries = new ArrayList<>(); @@ -122,7 +113,7 @@ public class ExceptionWorker { else { virtualFiles = virtualFilesInContent; } - HyperlinkInfo linkInfo = HyperlinkInfoFactory.getInstance().createMultipleFilesHyperlinkInfo(virtualFiles, lineNumber - 1, myProject); + HyperlinkInfo linkInfo = HyperlinkInfoFactory.getInstance().createMultipleFilesHyperlinkInfo(virtualFiles, myInfo.lineNumber - 1, myProject); Filter.Result result = new Filter.Result(highlightStartOffset, highlightEndOffset, linkInfo, attributes); myResult = result; return result; @@ -158,7 +149,7 @@ public class ExceptionWorker { return ArrayUtil.getFirstElement(myFiles); } - public Trinity getInfo() { + public ParsedLine getInfo() { return myInfo; } @@ -183,41 +174,94 @@ public class ExceptionWorker { } @Nullable - public static Trinity parseExceptionLine(final String line) { - int startIdx = findAtPrefix(line); - - TextRange yourKitLink = startIdx < 0 ? getYourKitLinkRange(line) : null; - int rParenIdx = yourKitLink != null ? yourKitLink.getEndOffset() - 2 : findFirstRParenAfterDigit(line); - if (rParenIdx < 0) return null; - - final int lParenIdx = line.lastIndexOf('(', rParenIdx); - if (lParenIdx < 0) return null; - - final int dotIdx = line.lastIndexOf('.', lParenIdx); - if (dotIdx < 0 || dotIdx < startIdx) return null; - int moduleIdx = line.indexOf('/'); - int classNameIdx = moduleIdx > -1 && moduleIdx < lParenIdx && moduleIdx < dotIdx ? moduleIdx + 1 : startIdx + 1 + (startIdx >= 0 ? AT.length() : 0); - - // class, method, link - return Trinity.create(new TextRange(classNameIdx, handleSpaces(line, dotIdx, -1)), - new TextRange(handleSpaces(line, dotIdx + 1, 1), handleSpaces(line, lParenIdx, -1)), - yourKitLink != null ? yourKitLink : new TextRange(lParenIdx + 1, rParenIdx)); + public static ParsedLine parseExceptionLine(final String line) { + ParsedLine result = parseNormalStackTraceLine(line); + if (result == null) result = parseYourKitLine(line); + if (result == null) result = parseForcedLine(line); + return result; } @Nullable - private static TextRange getYourKitLinkRange(String line) { + private static ParsedLine parseNormalStackTraceLine(String line) { + int startIdx = findAtPrefix(line); + int rParenIdx = findFirstRParenAfterDigit(line); + if (rParenIdx < 0) return null; + + TextRange methodName = findMethodNameCandidateBefore(line, startIdx, rParenIdx); + if (methodName == null) return null; + + int lParenIdx = methodName.getEndOffset(); + int dotIdx = methodName.getStartOffset() - 1; + int moduleIdx = line.indexOf('/'); + int classNameIdx = moduleIdx > -1 && moduleIdx < dotIdx ? moduleIdx + 1 : startIdx + 1 + (startIdx >= 0 ? AT.length() : 0); + + return ParsedLine.createFromFileAndLine(new TextRange(classNameIdx, handleSpaces(line, dotIdx, -1)), + trimRange(line, methodName), + lParenIdx + 1, rParenIdx, line); + } + + private static TextRange trimRange(String line, TextRange range) { + int start = handleSpaces(line, range.getStartOffset(), 1); + int end = handleSpaces(line, range.getEndOffset(), -1); + if (start != range.getStartOffset() || end != range.getEndOffset()) { + return TextRange.create(start, end); + } + return range; + } + + @Nullable + private static ParsedLine parseYourKitLine(String line) { int lineEnd = line.length() - 1; if (lineEnd > 0 && line.charAt(lineEnd) == '\n') lineEnd--; if (lineEnd > 0 && Character.isDigit(line.charAt(lineEnd))) { int spaceIndex = line.lastIndexOf(' '); int rParenIdx = line.lastIndexOf(')'); if (rParenIdx > 0 && spaceIndex == rParenIdx + 1) { - return new TextRange(spaceIndex + 1, lineEnd + 1); + TextRange methodName = findMethodNameCandidateBefore(line, 0, rParenIdx); + if (methodName != null) { + return ParsedLine.createFromFileAndLine(new TextRange(0, methodName.getStartOffset() - 1), + methodName, + spaceIndex + 1, lineEnd + 1, + line); + } } } return null; } + @Nullable + private static ParsedLine parseForcedLine(String line) { + String dash = "- "; + if (!line.trim().startsWith(dash)) return null; + + String linePrefix = "line="; + int lineNumberStart = line.indexOf(linePrefix); + if (lineNumberStart < 0) return null; + + int lineNumberEnd = line.indexOf(' ', lineNumberStart); + if (lineNumberEnd < 0) return null; + + TextRange methodName = findMethodNameCandidateBefore(line, 0, lineNumberStart); + if (methodName == null) return null; + + int lineNumber = getLineNumber(line.substring(lineNumberStart + linePrefix.length(), lineNumberEnd)); + if (lineNumber < 0) return null; + + return new ParsedLine(trimRange(line, TextRange.create(line.indexOf(dash) + dash.length(), methodName.getStartOffset() - 1)), + methodName, + TextRange.create(lineNumberStart, lineNumberEnd), null, lineNumber); + } + + private static TextRange findMethodNameCandidateBefore(String line, int start, int end) { + int lParenIdx = line.lastIndexOf('(', end); + if (lParenIdx < 0) return null; + + int dotIdx = line.lastIndexOf('.', lParenIdx); + if (dotIdx < 0 || dotIdx < start) return null; + + return TextRange.create(dotIdx + 1, lParenIdx); + } + private static int handleSpaces(String line, int pos, int delta) { int len = line.length(); while (pos >= 0 && pos < len) { @@ -227,4 +271,38 @@ public class ExceptionWorker { } return pos; } + + public static class ParsedLine { + @NotNull public final TextRange classFqnRange; + @NotNull public final TextRange methodNameRange; + @NotNull public final TextRange fileLineRange; + @Nullable public final String fileName; + public final int lineNumber; + + ParsedLine(@NotNull TextRange classFqnRange, + @NotNull TextRange methodNameRange, + @NotNull TextRange fileLineRange, @Nullable String fileName, int lineNumber) { + this.classFqnRange = classFqnRange; + this.methodNameRange = methodNameRange; + this.fileLineRange = fileLineRange; + this.fileName = fileName; + this.lineNumber = lineNumber; + } + + @Nullable + private static ParsedLine createFromFileAndLine(@NotNull TextRange classFqnRange, + @NotNull TextRange methodNameRange, + int fileLineStart, int fileLineEnd, String line) { + TextRange fileLineRange = TextRange.create(fileLineStart, fileLineEnd); + String fileAndLine = fileLineRange.substring(line); + + int colonIndex = fileAndLine.lastIndexOf(':'); + if (colonIndex < 0) return null; + + int lineNumber = getLineNumber(fileAndLine.substring(colonIndex + 1)); + if (lineNumber < 0) return null; + + return new ParsedLine(classFqnRange, methodNameRange, fileLineRange, fileAndLine.substring(0, colonIndex).trim(), lineNumber); + } + } }