@@ -6,6 +6,7 @@ import { CancellationToken, commands, DocumentLink, DocumentLinkProvider, Docume
66 window , workspace } from "vscode" ;
77import { instrumentOperationAsVsCodeCommand , sendInfo } from "vscode-extension-telemetry-wrapper" ;
88import { resolveSourceUri } from "./languageServerPlugin" ;
9+ import { parseJavaStackFrame } from "./stackFrameParser" ;
910import { getJavaExtensionAPI , isJavaExtEnabled , ServerMode } from "./utility" ;
1011
1112const ANALYZE_STACK_TRACE_COMMAND = "java.debug.analyzeStackTrace" ;
@@ -18,10 +19,6 @@ const STACK_TRACE_DOCUMENT_SELECTOR: DocumentSelector = [
1819 { scheme : "untitled" } ,
1920] ;
2021
21- // Matches a Java stack frame such as `at module/com.foo.Bar.baz(Bar.java:42)`.
22- // Group 2: optional module prefix, group 3: fully-qualified method, group 5: `File.java:line`.
23- const STACK_FRAME_REGEX = / ( \s a t \s + ) ( [ \w $ . ] + \/ ) ? ( ( [ \w $ ] + \. ) + [ < \w $ > ] + ) \( ( [ \w - $ ] + \. j a v a : \d + ) \) / ;
24-
2522// Guard against pathological input: cap the length of a scanned line (mitigates ReDoS on the
2623// nested-quantifier regex) and the number of links produced for very large pasted traces.
2724const MAX_SCANNED_LINE_LENGTH = 1000 ;
@@ -58,17 +55,21 @@ export class JavaStackTraceLinkProvider implements DocumentLinkProvider {
5855 continue ;
5956 }
6057
61- const result = STACK_FRAME_REGEX . exec ( lineText ) ;
62- if ( ! result || ! result . length ) {
58+ const frame = parseJavaStackFrame ( lineText ) ;
59+ if ( ! frame ) {
6360 continue ;
6461 }
6562
66- const stackTrace = ` ${ result [ 2 ] || "" } ${ result [ 3 ] } ( ${ result [ 5 ] } )` ;
67- const lineNumber = Number ( result [ 5 ] . split ( ":" ) [ 1 ] ) ;
68- const startIndex = result . index + result [ 1 ] . length ;
69- const range = new Range ( new Position ( i , startIndex ) , new Position ( i , startIndex + stackTrace . length ) ) ;
63+ const range = new Range (
64+ new Position ( i , frame . startIndex ) ,
65+ new Position ( i , frame . startIndex + frame . length ) ,
66+ ) ;
7067
71- const args : IStackFrameLinkArgs = { stackTrace, methodName : result [ 3 ] , lineNumber } ;
68+ const args : IStackFrameLinkArgs = {
69+ stackTrace : frame . stackTrace ,
70+ methodName : frame . methodName ,
71+ lineNumber : frame . lineNumber ,
72+ } ;
7273 const target = Uri . parse ( `command:${ NAVIGATE_TO_STACK_FRAME_COMMAND } ?${ encodeURIComponent ( JSON . stringify ( args ) ) } ` ) ;
7374 links . push ( new DocumentLink ( range , target ) ) ;
7475 }
@@ -97,22 +98,28 @@ async function navigateToStackFrame(args: IStackFrameLinkArgs): Promise<void> {
9798 */
9899 sendInfo ( "" , { operationName : "navigateToJavaStackFrame" } ) ;
99100
100- const uri = await resolveSourceUri ( args . stackTrace ) ;
101- if ( uri ) {
102- const parsed = Uri . parse ( uri ) ;
103- if ( ! ALLOWED_SOURCE_SCHEMES . has ( parsed . scheme ) ) {
104- return ;
101+ try {
102+ const uri = await resolveSourceUri ( args . stackTrace ) ;
103+ if ( uri ) {
104+ const parsed = Uri . parse ( uri ) ;
105+ if ( ! ALLOWED_SOURCE_SCHEMES . has ( parsed . scheme ) ) {
106+ return ;
107+ }
108+ const targetLine = Math . max ( args . lineNumber - 1 , 0 ) ;
109+ await window . showTextDocument ( parsed , {
110+ preserveFocus : true ,
111+ selection : new Range ( new Position ( targetLine , 0 ) , new Position ( targetLine , 0 ) ) ,
112+ } ) ;
113+ } else {
114+ // No source found: open the symbol quick pick scoped to the class name.
115+ const fullyQualifiedName = args . methodName . substring ( 0 , args . methodName . lastIndexOf ( "." ) ) ;
116+ const className = fullyQualifiedName . substring ( fullyQualifiedName . lastIndexOf ( "." ) + 1 ) ;
117+ await commands . executeCommand ( "workbench.action.quickOpen" , "#" + className ) ;
105118 }
106- const targetLine = Math . max ( args . lineNumber - 1 , 0 ) ;
107- window . showTextDocument ( parsed , {
108- preserveFocus : true ,
109- selection : new Range ( new Position ( targetLine , 0 ) , new Position ( targetLine , 0 ) ) ,
110- } ) ;
111- } else {
112- // No source found: open the symbol quick pick scoped to the class name.
113- const fullyQualifiedName = args . methodName . substring ( 0 , args . methodName . lastIndexOf ( "." ) ) ;
114- const className = fullyQualifiedName . substring ( fullyQualifiedName . lastIndexOf ( "." ) + 1 ) ;
115- commands . executeCommand ( "workbench.action.quickOpen" , "#" + className ) ;
119+ } catch {
120+ // The internal navigate command is always registered, but resolving a frame needs the Java
121+ // language server in Standard mode. If it isn't (e.g. server restarting/downgraded) or the
122+ // resolved document fails to open, fail quietly instead of surfacing an unhandled rejection.
116123 }
117124}
118125
@@ -124,7 +131,8 @@ async function analyzeStackTrace(): Promise<void> {
124131 // The command itself is auto-instrumented via instrumentOperationAsVsCodeCommand, so no
125132 // manual telemetry is needed here to track invocations.
126133 const clipboard = await env . clipboard . readText ( ) ;
127- const content = STACK_FRAME_REGEX . test ( clipboard . slice ( 0 , MAX_CLIPBOARD_SCAN_LENGTH ) ) ? clipboard : "" ;
134+ const looksLikeTrace = parseJavaStackFrame ( clipboard . slice ( 0 , MAX_CLIPBOARD_SCAN_LENGTH ) ) !== undefined ;
135+ const content = looksLikeTrace ? clipboard : "" ;
128136 const document = await workspace . openTextDocument ( { language : "log" , content } ) ;
129137 await window . showTextDocument ( document ) ;
130138}
0 commit comments