@@ -26,39 +26,69 @@ export function parseApex(apexCode: string): ApexNode {
2626 return new ApexVisitor ( ) . visit ( parser . compilationUnit ( ) ) ;
2727}
2828
29- export function getMethodLine ( rootNode : ApexNode , symbols : string [ ] ) : SymbolLocation {
29+ export function getMethodLine ( rootNode : ApexNode , fullyQualifiedSymbol : string ) : SymbolLocation {
3030 const result : SymbolLocation = { character : 0 , line : 1 , isExactMatch : true } ;
3131
32- if ( symbols [ 0 ] === rootNode . name ) {
33- symbols = symbols . slice ( 1 ) ;
32+ if ( fullyQualifiedSymbol . indexOf ( '(' ) === - 1 ) {
33+ return result ;
3434 }
3535
36- if ( ! symbols . length ) {
37- return result ;
36+ // NOTE: The symbol may contain namespaces as symbols from the debug log are fully qualified e.g myns.MyClass.InnerClass.method(args)
37+ // We are attempting rudamentary handling of the case where symbols contain namespace but the parsed class does not but no guarantees it work in all cases.
38+
39+ // There are two possible symbol types method and constructor, args are optional
40+ // MyClass.method(args) - method, MyClass.InnerClass.method(args) - method
41+ // MyClass(args) - constuctor, MyClass.InnerClass(args) - constuctor
42+
43+ // Find the Namespace of the supplied symbol, if there is one.
44+ const outerClassNode = rootNode . children ?. [ 0 ] ;
45+ let outerClassName = outerClassNode ?. name ?? '' ;
46+ const endNsIndex = fullyQualifiedSymbol . indexOf ( outerClassName ) ;
47+ const namespace = endNsIndex > 0 ? fullyQualifiedSymbol . slice ( 0 , endNsIndex - 1 ) : '' ;
48+ if ( namespace ) {
49+ outerClassName = namespace + '.' + outerClassName ;
50+ // Remove the leading namespace as most likely the source code will not have it, for symbols in this file.
51+ fullyQualifiedSymbol = fullyQualifiedSymbol . replace ( namespace + '.' , '' ) ;
3852 }
3953
54+ // strip all whitespace to make comparisons easier
55+ fullyQualifiedSymbol = fullyQualifiedSymbol . replaceAll ( ' ' , '' ) . toLowerCase ( ) ;
56+
57+ // This is the index of the first '(' which indicates method args or constructor args.
58+ const methodArgsIndex = fullyQualifiedSymbol . indexOf ( '(' ) ;
59+ // We can't tell the difference between InnerClass constructor and outer class method call.
60+ // As such className could either be the class name or the method name, we need to check.
61+ const className = fullyQualifiedSymbol . slice ( 0 , methodArgsIndex ) ;
4062 let currentRoot : ApexNode | undefined = rootNode ;
63+ // Keep iterating until we find the last symbol that is a class.
64+ // The next symbol might be a method or might be invalid.
65+ for ( const symbol of className . split ( '.' ) ) {
66+ const nextRoot = findClassNode ( currentRoot , symbol , namespace ) ;
67+ if ( ! nextRoot ) {
68+ break ;
69+ }
4170
42- for ( const symbol of symbols ) {
43- if ( isClassSymbol ( symbol ) ) {
44- currentRoot = findClassNode ( currentRoot , symbol ) ;
71+ currentRoot = nextRoot ;
72+ }
4573
46- if ( ! currentRoot ) {
47- result . isExactMatch = false ;
48- result . missingSymbol = symbol ;
49- break ;
50- }
51- } else {
52- const methodNode = findMethodNode ( currentRoot , symbol ) ;
74+ if ( currentRoot ) {
75+ result . line = currentRoot . line ?? 1 ;
76+ result . character = currentRoot . idCharacter ?? 0 ;
77+ }
5378
54- if ( ! methodNode ) {
55- result . line = currentRoot . line ?? 1 ;
56- result . character = currentRoot . idCharacter ?? 0 ;
57- result . isExactMatch = false ;
58- result . missingSymbol = symbol ;
59- break ;
60- }
79+ // TODO: enchance to find constructors as well as methods
6180
81+ // This is the method name before the args list, this may actually be a class name though so we need to check.
82+ // e.g for MyClass.InnerClass(args) we get InnerClass(args) but is this a method of InnerClass constructor?
83+ const qualifiedMethodName = fullyQualifiedSymbol . slice ( className . lastIndexOf ( '.' ) + 1 ) ;
84+ if ( qualifiedMethodName && currentRoot ) {
85+ const methodNode = findMethodNode ( currentRoot , qualifiedMethodName , outerClassName ) ;
86+
87+ if ( ! methodNode ) {
88+ result . line = currentRoot . line ?? 1 ;
89+ result . isExactMatch = false ;
90+ result . missingSymbol = qualifiedMethodName ;
91+ } else {
6292 result . line = methodNode . line ;
6393 result . character = methodNode . idCharacter ;
6494 }
@@ -67,22 +97,50 @@ export function getMethodLine(rootNode: ApexNode, symbols: string[]): SymbolLoca
6797 return result ;
6898}
6999
70- function isClassSymbol ( symbol : string ) : boolean {
71- return ! symbol . includes ( '(' ) ;
72- }
100+ function findClassNode ( root : ApexNode , symbol : string , namespace : string ) : ApexNode | undefined {
101+ const classNode = root . children ?. find (
102+ ( child ) => child . name === symbol && child . nature === 'Class' ,
103+ ) ;
104+ if ( classNode ) {
105+ return classNode ;
106+ }
107+
108+ if ( namespace ) {
109+ return root . children ?. find (
110+ ( child ) => child . name === symbol . replaceAll ( namespace + '.' , '' ) && child . nature === 'Class' ,
111+ ) ;
112+ }
73113
74- function findClassNode ( root : ApexNode , symbol : string ) : ApexNode | undefined {
75- return root . children ?. find ( ( child ) => child . name === symbol && child . nature === 'Class' ) ;
114+ return undefined ;
76115}
77116
78- function findMethodNode ( root : ApexNode , symbol : string ) : ApexMethodNode | undefined {
79- const [ methodName , params ] = symbol . split ( '(' ) ;
80- const paramStr = params ?. replace ( ')' , '' ) . trim ( ) ;
117+ function findMethodNode (
118+ root : ApexNode ,
119+ symbol : string ,
120+ outerClassName : string ,
121+ ) : ApexMethodNode | undefined {
122+ const [ methodName , args = '' ] = symbol . slice ( 0 , - 1 ) . split ( '(' ) ;
123+ let params = args ;
124+
125+ const methodNode = root . children ?. find (
126+ ( child ) =>
127+ child . name === methodName &&
128+ child . nature === 'Method' &&
129+ ( params === undefined || ( child as ApexMethodNode ) . params . toLowerCase ( ) === params ) ,
130+ ) as ApexMethodNode ;
131+
132+ if ( methodNode ) {
133+ return methodNode ;
134+ }
81135
136+ // Try again but with the class name removed from args list. args from the debug log are fully qualified but they are not necessarily in the file,
137+ // as we only need to qualify for external types to the file.
138+ // (MyClass.ObjectArg) vs (ObjectArg) where MyClass is current class.
139+ params = params . replaceAll ( outerClassName + '.' , '' ) ;
82140 return root . children ?. find (
83141 ( child ) =>
84142 child . name === methodName &&
85143 child . nature === 'Method' &&
86- ( paramStr === undefined || ( child as ApexMethodNode ) . params === paramStr ) ,
144+ ( params === undefined || ( child as ApexMethodNode ) . params . toLowerCase ( ) === params ) ,
87145 ) as ApexMethodNode ;
88146}
0 commit comments