@@ -8,7 +8,12 @@ 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 = {
1419 character : number ;
@@ -27,7 +32,7 @@ export function parseApex(apexCode: string): ApexNode {
2732}
2833
2934export function getMethodLine ( rootNode : ApexNode , fullyQualifiedSymbol : string ) : SymbolLocation {
30- const result : SymbolLocation = { character : 0 , line : 1 , isExactMatch : true } ;
35+ const result : SymbolLocation = { character : 0 , line : 1 , isExactMatch : false } ;
3136
3237 if ( fullyQualifiedSymbol . indexOf ( '(' ) === - 1 ) {
3338 return result ;
@@ -41,24 +46,23 @@ export function getMethodLine(rootNode: ApexNode, fullyQualifiedSymbol: string):
4146 // MyClass(args) - constuctor, MyClass.InnerClass(args) - constuctor
4247
4348 // Find the Namespace of the supplied symbol, if there is one.
49+ // strip all whitespace to make comparisons easier
50+ let symbolToFind = normalizeText ( fullyQualifiedSymbol ) ;
4451 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 ) : '' ;
52+ let outerClassName = normalizeText ( outerClassNode ?. name ?? '' ) ;
53+ const endNsIndex = symbolToFind . indexOf ( outerClassName ) ;
54+ const namespace = endNsIndex > 0 ? symbolToFind . slice ( 0 , endNsIndex - 1 ) : '' ;
4855 if ( namespace ) {
4956 outerClassName = namespace + '.' + outerClassName ;
5057 // Remove the leading namespace as most likely the source code will not have it, for symbols in this file.
51- fullyQualifiedSymbol = fullyQualifiedSymbol . replace ( namespace + '.' , '' ) ;
58+ symbolToFind = symbolToFind . replace ( namespace + '.' , '' ) ;
5259 }
5360
54- // strip all whitespace to make comparisons easier
55- fullyQualifiedSymbol = fullyQualifiedSymbol . replaceAll ( ' ' , '' ) . toLowerCase ( ) ;
56-
5761 // This is the index of the first '(' which indicates method args or constructor args.
58- const methodArgsIndex = fullyQualifiedSymbol . indexOf ( '(' ) ;
62+ const methodArgsIndex = symbolToFind . indexOf ( '(' ) ;
5963 // We can't tell the difference between InnerClass constructor and outer class method call.
6064 // As such className could either be the class name or the method name, we need to check.
61- const className = fullyQualifiedSymbol . slice ( 0 , methodArgsIndex ) ;
65+ const className = symbolToFind . slice ( 0 , methodArgsIndex ) ;
6266 let currentRoot : ApexNode | undefined = rootNode ;
6367 // Keep iterating until we find the last symbol that is a class.
6468 // The next symbol might be a method or might be invalid.
@@ -76,71 +80,94 @@ export function getMethodLine(rootNode: ApexNode, fullyQualifiedSymbol: string):
7680 result . character = currentRoot . idCharacter ?? 0 ;
7781 }
7882
79- // TODO: enchance to find constructors as well as methods
80-
8183 // This is the method name before the args list, this may actually be a class name though so we need to check.
8284 // 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 ) ;
85+ const qualifiedMethodName = symbolToFind . slice ( className . lastIndexOf ( '.' ) + 1 ) ;
8486 if ( qualifiedMethodName && currentRoot ) {
85- const methodNode = findMethodNode ( currentRoot , qualifiedMethodName , outerClassName ) ;
86-
87+ let methodNode : ApexMethodNode | ApexConstructorNode | undefined = findMethodNode (
88+ currentRoot ,
89+ qualifiedMethodName ,
90+ outerClassName ,
91+ ) ;
8792 if ( ! methodNode ) {
88- result . line = currentRoot . line ?? 1 ;
89- result . isExactMatch = false ;
90- result . missingSymbol = qualifiedMethodName ;
91- } else {
93+ methodNode = findConstructorNode ( currentRoot , qualifiedMethodName , outerClassName ) ;
94+ }
95+
96+ if ( methodNode ) {
9297 result . line = methodNode . line ;
9398 result . character = methodNode . idCharacter ;
99+ result . isExactMatch = true ;
100+ return result ;
94101 }
95102 }
96103
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 ) ;
97108 return result ;
98109}
99110
100111function 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- }
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+ }
113118
114- return undefined ;
119+ return false ;
120+ } ) ;
115121}
116122
117123function findMethodNode (
118124 root : ApexNode ,
119125 symbol : string ,
120126 outerClassName : string ,
121127) : 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- }
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 ;
144+ }
135145
146+ function findConstructorNode (
147+ root : ApexNode ,
148+ symbol : string ,
149+ outerClassName : string ,
150+ ) : ApexConstructorNode | undefined {
151+ const [ constructorName , params = '' ] = symbol . slice ( 0 , - 1 ) . split ( '(' ) ;
136152 // 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,
137153 // as we only need to qualify for external types to the file.
138154 // (MyClass.ObjectArg) vs (ObjectArg) where MyClass is current class.
139- params = params . replaceAll ( outerClassName + '.' , '' ) ;
140- return root . children ?. find (
141- ( child ) =>
142- child . name === methodName &&
143- child . nature === 'Method' &&
144- ( params === undefined || ( child as ApexMethodNode ) . params . toLowerCase ( ) === params ) ,
145- ) as ApexMethodNode ;
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+ }
170+
171+ function normalizeText ( text : string ) : string {
172+ return text ?. replaceAll ( ' ' , '' ) . toLowerCase ( ) ;
146173}
0 commit comments