Skip to content

Commit 92cb9d6

Browse files
committed
fix: Refactor getMethodLine to handle fully qualified symbols and improve method resolution
Done via a combination of - match arg list directly with string from log, if no match then try again with the outer class name stripped from the args since the source code probably does not reference it (but could) - strip spaces from arg list so they can also be compared
1 parent 58beb74 commit 92cb9d6

2 files changed

Lines changed: 97 additions & 35 deletions

File tree

lana/src/salesforce/ApexParser/ApexSymbolLocator.ts

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

lana/src/salesforce/ApexParser/ApexVisitor.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type ApexNature = 'Class' | 'Method';
1818
export interface ApexNode {
1919
/** The type of Apex construct (Class or Method) */
2020
nature?: ApexNature;
21-
/** The name of the class or method */
21+
/** The name of the class or method, in lower case */
2222
name?: string;
2323
/** Child nodes (nested classes or methods) */
2424
children?: ApexNode[];
@@ -87,7 +87,7 @@ export class ApexVisitor implements ApexParserVisitor<ApexNode> {
8787

8888
return {
8989
nature: 'Class',
90-
name: ctx.id().Identifier()?.toString() ?? '',
90+
name: ident.text.toLowerCase(),
9191
children: ctx.children?.length ? this.visitChildren(ctx).children : [],
9292
line: start.line,
9393
idCharacter: ident.start.charPositionInLine ?? 0,
@@ -100,7 +100,7 @@ export class ApexVisitor implements ApexParserVisitor<ApexNode> {
100100

101101
return {
102102
nature: 'Method',
103-
name: ctx.id().Identifier()?.toString() ?? '',
103+
name: ident.text.toLowerCase(),
104104
children: ctx.children?.length ? this.visitChildren(ctx).children : [],
105105
params: this.getParameters(ctx.formalParameters()),
106106
line: start.line,
@@ -118,7 +118,11 @@ export class ApexVisitor implements ApexParserVisitor<ApexNode> {
118118

119119
private getParameters(ctx: FormalParametersContext): string {
120120
const paramsList = ctx.formalParameterList()?.formalParameter();
121-
return paramsList?.map((param) => param.typeRef().typeName(0)?.text).join(', ') ?? '';
121+
return (
122+
paramsList
123+
?.map((param) => param.typeRef().text.replaceAll(' ', '').toLowerCase())
124+
.join(',') ?? ''
125+
);
122126
}
123127

124128
private forNode(node: ApexNode, anonHandler: (n: ApexNode) => void) {

0 commit comments

Comments
 (0)