@@ -8,9 +8,15 @@ import {
88 CommonTokenStream ,
99} from '@apexdevtools/apex-parser' ;
1010import { CharStreams } from 'antlr4ts' ;
11- import { ApexVisitor , type ApexMethodNode , type ApexNode } from './ApexVisitor' ;
11+ import {
12+ ApexVisitor ,
13+ type ApexConstructorNode ,
14+ type ApexMethodNode ,
15+ type ApexNode ,
16+ } from './ApexVisitor' ;
1217
1318export type SymbolLocation = {
19+ character : number ;
1420 line : number ;
1521 isExactMatch : boolean ;
1622 missingSymbol ?: string ;
@@ -25,61 +31,143 @@ export function parseApex(apexCode: string): ApexNode {
2531 return new ApexVisitor ( ) . visit ( parser . compilationUnit ( ) ) ;
2632}
2733
28- export function getMethodLine ( rootNode : ApexNode , symbols : string [ ] ) : SymbolLocation {
29- const result : SymbolLocation = { line : 1 , isExactMatch : true } ;
34+ export function getMethodLine ( rootNode : ApexNode , fullyQualifiedSymbol : string ) : SymbolLocation {
35+ const result : SymbolLocation = { character : 0 , line : 1 , isExactMatch : false } ;
3036
31- if ( symbols [ 0 ] === rootNode . name ) {
32- symbols = symbols . slice ( 1 ) ;
37+ if ( fullyQualifiedSymbol . indexOf ( '(' ) === - 1 ) {
38+ return result ;
3339 }
3440
35- if ( ! symbols . length ) {
36- return result ;
41+ // NOTE: The symbol may contain namespaces as symbols from the debug log are fully qualified e.g myns.MyClass.InnerClass.method(args)
42+ // 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.
43+
44+ // There are two possible symbol types method and constructor, args are optional
45+ // MyClass.method(args) - method, MyClass.InnerClass.method(args) - method
46+ // MyClass(args) - constuctor, MyClass.InnerClass(args) - constuctor
47+
48+ // Find the Namespace of the supplied symbol, if there is one.
49+ // strip all whitespace to make comparisons easier
50+ let symbolToFind = normalizeText ( fullyQualifiedSymbol ) ;
51+ const outerClassNode = rootNode . children ?. [ 0 ] ;
52+ let outerClassName = normalizeText ( outerClassNode ?. name ?? '' ) ;
53+ const endNsIndex = symbolToFind . indexOf ( outerClassName ) ;
54+ const namespace = endNsIndex > 0 ? symbolToFind . slice ( 0 , endNsIndex - 1 ) : '' ;
55+ if ( namespace ) {
56+ outerClassName = namespace + '.' + outerClassName ;
57+ // Remove the leading namespace as most likely the source code will not have it, for symbols in this file.
58+ symbolToFind = symbolToFind . replace ( namespace + '.' , '' ) ;
3759 }
3860
61+ // This is the index of the first '(' which indicates method args or constructor args.
62+ const methodArgsIndex = symbolToFind . indexOf ( '(' ) ;
63+ // We can't tell the difference between InnerClass constructor and outer class method call.
64+ // As such className could either be the class name or the method name, we need to check.
65+ const className = symbolToFind . slice ( 0 , methodArgsIndex ) ;
3966 let currentRoot : ApexNode | undefined = rootNode ;
67+ // Keep iterating until we find the last symbol that is a class.
68+ // The next symbol might be a method or might be invalid.
69+ for ( const symbol of className . split ( '.' ) ) {
70+ const nextRoot = findClassNode ( currentRoot , symbol , namespace ) ;
71+ if ( ! nextRoot ) {
72+ break ;
73+ }
74+
75+ currentRoot = nextRoot ;
76+ }
77+
78+ if ( currentRoot ) {
79+ result . line = currentRoot . line ?? 1 ;
80+ result . character = currentRoot . idCharacter ?? 0 ;
81+ }
4082
41- for ( const symbol of symbols ) {
42- if ( isClassSymbol ( symbol ) ) {
43- currentRoot = findClassNode ( currentRoot , symbol ) ;
44-
45- if ( ! currentRoot ) {
46- result . isExactMatch = false ;
47- result . missingSymbol = symbol ;
48- break ;
49- }
50- } else {
51- const methodNode = findMethodNode ( currentRoot , symbol ) ;
52-
53- if ( ! methodNode ) {
54- result . line = currentRoot . line ?? 1 ;
55- result . isExactMatch = false ;
56- result . missingSymbol = symbol ;
57- break ;
58- }
83+ // This is the method name before the args list, this may actually be a class name though so we need to check.
84+ // e.g for MyClass.InnerClass(args) we get InnerClass(args) but is this a method of InnerClass constructor?
85+ const qualifiedMethodName = symbolToFind . slice ( className . lastIndexOf ( '.' ) + 1 ) ;
86+ if ( qualifiedMethodName && currentRoot ) {
87+ let methodNode : ApexMethodNode | ApexConstructorNode | undefined = findMethodNode (
88+ currentRoot ,
89+ qualifiedMethodName ,
90+ outerClassName ,
91+ ) ;
92+ if ( ! methodNode ) {
93+ methodNode = findConstructorNode ( currentRoot , qualifiedMethodName , outerClassName ) ;
94+ }
5995
96+ if ( methodNode ) {
6097 result . line = methodNode . line ;
98+ result . character = methodNode . idCharacter ;
99+ result . isExactMatch = true ;
100+ return result ;
61101 }
62102 }
63103
104+ result . line = currentRoot . line ?? 1 ;
105+ result . isExactMatch = false ;
106+ // keep the original case for error messages.
107+ result . missingSymbol = fullyQualifiedSymbol . slice ( className . lastIndexOf ( '.' ) + 1 ) ;
64108 return result ;
65109}
66110
67- function isClassSymbol ( symbol : string ) : boolean {
68- return ! symbol . includes ( '(' ) ;
111+ function findClassNode ( root : ApexNode , symbol : string , namespace : string ) : ApexNode | undefined {
112+ const symbolWithoutNamespace = symbol . replaceAll ( namespace + '.' , '' ) ;
113+ return root . children ?. find ( ( child ) => {
114+ if ( child . nature === 'Class' ) {
115+ const normalizedChildName = normalizeText ( child . name ?? '' ) ;
116+ return normalizedChildName === symbol || normalizedChildName === symbolWithoutNamespace ;
117+ }
118+
119+ return false ;
120+ } ) ;
69121}
70122
71- function findClassNode ( root : ApexNode , symbol : string ) : ApexNode | undefined {
72- return root . children ?. find ( ( child ) => child . name === symbol && child . nature === 'Class' ) ;
123+ function findMethodNode (
124+ root : ApexNode ,
125+ symbol : string ,
126+ outerClassName : string ,
127+ ) : ApexMethodNode | undefined {
128+ const [ methodName , params = '' ] = symbol . slice ( 0 , - 1 ) . split ( '(' ) ;
129+ // 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,
130+ // as we only need to qualify for external types to the file.
131+ // (MyClass.ObjectArg) vs (ObjectArg) where MyClass is current class.
132+ const paramsWithoutClassName = params . replaceAll ( outerClassName + '.' , '' ) ;
133+
134+ return root . children ?. find ( ( child ) => {
135+ if ( child . nature === 'Method' && normalizeText ( child . name ?? '' ) === methodName ) {
136+ const methodChild = child as ApexMethodNode ;
137+ const methodParams = normalizeText ( methodChild . params ) ;
138+ return (
139+ params === undefined || methodParams === params || methodParams === paramsWithoutClassName
140+ ) ;
141+ }
142+ return false ;
143+ } ) as ApexMethodNode ;
73144}
74145
75- function findMethodNode ( root : ApexNode , symbol : string ) : ApexMethodNode | undefined {
76- const [ methodName , params ] = symbol . split ( '(' ) ;
77- const paramStr = params ?. replace ( ')' , '' ) . trim ( ) ;
146+ function findConstructorNode (
147+ root : ApexNode ,
148+ symbol : string ,
149+ outerClassName : string ,
150+ ) : ApexConstructorNode | undefined {
151+ const [ constructorName , params = '' ] = symbol . slice ( 0 , - 1 ) . split ( '(' ) ;
152+ // 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,
153+ // as we only need to qualify for external types to the file.
154+ // (MyClass.ObjectArg) vs (ObjectArg) where MyClass is current class.
155+ const paramsWithoutClassName = params . replaceAll ( outerClassName + '.' , '' ) ;
156+
157+ return root . children ?. find ( ( child ) => {
158+ if ( child . nature === 'Constructor' && normalizeText ( child . name ?? '' ) === constructorName ) {
159+ const constructorChild = child as ApexConstructorNode ;
160+ const constructorParams = normalizeText ( constructorChild . params ) ;
161+ return (
162+ params === undefined ||
163+ constructorParams === params ||
164+ constructorParams === paramsWithoutClassName
165+ ) ;
166+ }
167+ return false ;
168+ } ) as ApexConstructorNode ;
169+ }
78170
79- return root . children ?. find (
80- ( child ) =>
81- child . name === methodName &&
82- child . nature === 'Method' &&
83- ( paramStr === undefined || ( child as ApexMethodNode ) . params === paramStr ) ,
84- ) as ApexMethodNode ;
171+ function normalizeText ( text : string ) : string {
172+ return text ?. replaceAll ( ' ' , '' ) . toLowerCase ( ) ;
85173}
0 commit comments