Skip to content

Commit 3d9ee04

Browse files
committed
Improve apex bind variable parsing #155
resolves #155
1 parent d4afaab commit 3d9ee04

File tree

7 files changed

+189
-8
lines changed

7 files changed

+189
-8
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Changelog
22

3-
## 4.1.0
3+
## 4.2.0
4+
5+
June 7, 2021
6+
7+
#155 - Apex bind variable support is improved to allow parsing of more complex Apex.
8+
9+
## 4.1.1
410

511
June 6, 2021
612

src/models.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,31 @@ export interface ExpressionContext {
201201
}
202202

203203
export interface ApexBindVariableExpressionContext {
204-
Identifier: IToken[];
204+
apex: (CstNode | IToken)[];
205+
COLON: IToken[];
206+
DECIMAL?: IToken[];
207+
}
208+
209+
export interface ApexBindVariableNewInstantiationContext {
210+
NEW: IToken[];
211+
FUNCTION: IToken[];
212+
apexBindVariableGeneric?: CstNode[];
213+
apexBindVariableFunctionParams: CstNode[];
214+
}
215+
export interface ApexBindVariableFunctionCallContext {
216+
FUNCTION: IToken[];
217+
apexBindVariableFunctionParams: CstNode[];
218+
}
219+
export interface ApexBindVariableGenericContext {
220+
COMMA: IToken[];
221+
GREATER_THAN: IToken[];
222+
LESS_THAN: IToken[];
223+
PARAMETER: IToken[];
224+
}
225+
export interface ApexBindVariableFunctionParamsContext {
226+
L_PAREN: IToken[];
227+
R_PAREN: IToken[];
228+
PARAMETER?: IToken[];
205229
}
206230

207231
export interface ExpressionOperatorContext {

src/parser/lexer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ export const AboveOrBelow = createToken({
263263
longer_alt: Identifier,
264264
categories: [Keyword, Identifier],
265265
});
266+
export const ApexNew = createToken({ name: 'new', pattern: /new/i, longer_alt: Identifier, categories: [Keyword, Identifier] });
266267
export const At = createToken({ name: 'AT', pattern: /AT/i, longer_alt: Identifier, categories: [Keyword, Identifier] });
267268
export const Below = createToken({ name: 'BELOW', pattern: /BELOW/i, longer_alt: Identifier, categories: [Keyword, Identifier] });
268269
export const DataCategory = createToken({
@@ -913,6 +914,7 @@ export const allTokens = [
913914

914915
AboveOrBelow,
915916
Above,
917+
ApexNew,
916918
At,
917919
Below,
918920
DataCategory,

src/parser/parser.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class SoqlParser extends CstParser {
3333
private $_aggregateFunction: any = undefined;
3434
private $_otherFunction: any = undefined;
3535
private $_atomicExpression: any = undefined;
36+
private $_apexBindVariableExpression: any = undefined;
3637
private $_arrayExpression: any = undefined;
3738
private $_relationalOperator: any = undefined;
3839
private $_selectClause: any = undefined;
@@ -617,7 +618,62 @@ export class SoqlParser extends CstParser {
617618

618619
private apexBindVariableExpression = this.RULE('apexBindVariableExpression', () => {
619620
this.CONSUME(lexer.Colon);
620-
this.CONSUME(lexer.Identifier);
621+
// First item in list could optionally be a new instantiation
622+
this.OPTION(() => {
623+
this.SUBRULE(this.apexBindVariableNewInstantiation, { LABEL: 'apex' });
624+
this.OPTION1(() => {
625+
this.CONSUME(lexer.Decimal);
626+
});
627+
});
628+
// Chained function calls or nested arguments with function calls at the end
629+
this.MANY_SEP({
630+
SEP: lexer.Decimal,
631+
DEF: () => {
632+
this.OR(
633+
this.$_apexBindVariableExpression ||
634+
(this.$_apexBindVariableExpression = [
635+
{ ALT: () => this.SUBRULE(this.apexBindVariableFunctionCall, { LABEL: 'apex' }) },
636+
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'apex' }) },
637+
]),
638+
);
639+
},
640+
});
641+
});
642+
643+
private apexBindVariableNewInstantiation = this.RULE('apexBindVariableNewInstantiation', () => {
644+
this.CONSUME(lexer.ApexNew, { LABEL: 'NEW' });
645+
this.CONSUME(lexer.Identifier, { LABEL: 'FUNCTION' });
646+
this.OPTION(() => {
647+
this.SUBRULE(this.apexBindVariableGeneric);
648+
});
649+
this.SUBRULE(this.apexBindVariableFunctionParams);
650+
});
651+
652+
private apexBindVariableFunctionCall = this.RULE('apexBindVariableFunctionCall', () => {
653+
this.CONSUME(lexer.Identifier, { LABEL: 'FUNCTION' });
654+
this.SUBRULE(this.apexBindVariableFunctionParams);
655+
});
656+
657+
private apexBindVariableGeneric = this.RULE('apexBindVariableGeneric', () => {
658+
this.CONSUME(lexer.LessThan);
659+
this.AT_LEAST_ONE_SEP({
660+
SEP: lexer.Comma,
661+
DEF: () => {
662+
this.CONSUME(lexer.Identifier, { LABEL: 'PARAMETER' });
663+
},
664+
});
665+
this.CONSUME(lexer.GreaterThan);
666+
});
667+
668+
private apexBindVariableFunctionParams = this.RULE('apexBindVariableFunctionParams', () => {
669+
this.CONSUME(lexer.LParen);
670+
this.MANY_SEP({
671+
SEP: lexer.Comma,
672+
DEF: () => {
673+
this.CONSUME(lexer.Identifier, { LABEL: 'PARAMETER' });
674+
},
675+
});
676+
this.CONSUME(lexer.RParen);
621677
});
622678

623679
private arrayExpression = this.RULE('arrayExpression', () => {

src/parser/visitor.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import {
3434
} from '../api/api-models';
3535
import {
3636
ApexBindVariableExpressionContext,
37+
ApexBindVariableFunctionCallContext,
38+
ApexBindVariableFunctionParamsContext,
39+
ApexBindVariableGenericContext,
40+
ApexBindVariableNewInstantiationContext,
3741
ArrayExpressionWithType,
3842
AtomicExpressionContext,
3943
BooleanContext,
@@ -791,7 +795,29 @@ class SOQLVisitor extends BaseSoqlVisitor {
791795
}
792796

793797
apexBindVariableExpression(ctx: ApexBindVariableExpressionContext): string {
794-
return ctx.Identifier[0].image;
798+
return ctx.apex.map(item => (isToken(item) ? item.image : this.visit(item))).join('.');
799+
}
800+
801+
apexBindVariableNewInstantiation(ctx: ApexBindVariableNewInstantiationContext): string {
802+
let output = `new ${ctx.FUNCTION[0].image}`;
803+
if (ctx.apexBindVariableGeneric) {
804+
output += this.visit(ctx.apexBindVariableGeneric[0]);
805+
}
806+
output += this.visit(ctx.apexBindVariableFunctionParams[0]);
807+
return output;
808+
}
809+
810+
apexBindVariableFunctionCall(ctx: ApexBindVariableFunctionCallContext): string {
811+
return `${ctx.FUNCTION[0].image}${this.visit(ctx.apexBindVariableFunctionParams[0])}`;
812+
}
813+
814+
apexBindVariableGeneric(ctx: ApexBindVariableGenericContext): string {
815+
return `<${ctx.PARAMETER.map(item => item.image).join(', ')}>`;
816+
}
817+
818+
apexBindVariableFunctionParams(ctx: ApexBindVariableFunctionParamsContext): string {
819+
const params = Array.isArray(ctx.PARAMETER) ? ctx.PARAMETER : [];
820+
return `(${params.map(item => item.image).join(', ')})`;
795821
}
796822

797823
arrayExpression(ctx: ValueContext): ArrayExpressionWithType[] {

test/test-cases.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,18 +2265,85 @@ export const testCases: TestCase[] = [
22652265
},
22662266
{
22672267
testCase: 112,
2268-
options: { allowApexBindVariables: true, ignoreParseErrors: true },
2268+
options: { allowApexBindVariables: true },
22692269
soql: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contactMap.keySet()) FROM Account WHERE Id IN :accountMap.keySet()`,
2270-
soqlComposed: `SELECT Id, (SELECT Id FROM Contacts) FROM Account`,
22712270
output: {
22722271
fields: [
22732272
{
22742273
type: 'Field',
22752274
field: 'Id',
22762275
},
2277-
{ type: 'FieldSubquery', subquery: { fields: [{ type: 'Field', field: 'Id' }], relationshipName: 'Contacts' } },
2276+
{
2277+
type: 'FieldSubquery',
2278+
subquery: {
2279+
fields: [{ type: 'Field', field: 'Id' }],
2280+
relationshipName: 'Contacts',
2281+
where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'contactMap.keySet()' } },
2282+
},
2283+
},
2284+
],
2285+
sObject: 'Account',
2286+
where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'accountMap.keySet()' } },
2287+
},
2288+
},
2289+
{
2290+
testCase: 113,
2291+
options: { allowApexBindVariables: true, ignoreParseErrors: true },
2292+
soql: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contact_900Map.keySet()) FROM Account WHERE Id IN :acco INVALID untMap.keySet()`,
2293+
soqlComposed: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contact_900Map.keySet()) FROM Account`,
2294+
output: {
2295+
fields: [
2296+
{
2297+
type: 'Field',
2298+
field: 'Id',
2299+
},
2300+
{
2301+
type: 'FieldSubquery',
2302+
subquery: {
2303+
fields: [{ type: 'Field', field: 'Id' }],
2304+
relationshipName: 'Contacts',
2305+
where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'contact_900Map.keySet()' } },
2306+
},
2307+
},
2308+
],
2309+
sObject: 'Account',
2310+
},
2311+
},
2312+
{
2313+
testCase: 114,
2314+
options: { allowApexBindVariables: true },
2315+
soql: `SELECT Id FROM Account WHERE Id IN :new Map<Id, SObject>(someVar).keySet()`,
2316+
output: {
2317+
fields: [
2318+
{
2319+
type: 'Field',
2320+
field: 'Id',
2321+
},
2322+
],
2323+
sObject: 'Account',
2324+
where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'new Map<Id, SObject>(someVar).keySet()' } },
2325+
},
2326+
},
2327+
{
2328+
testCase: 115,
2329+
options: { allowApexBindVariables: true },
2330+
soql: `SELECT Id FROM Account WHERE Id IN :new Map<Id, SObject>(someVar).getSomeClass().records`,
2331+
output: {
2332+
fields: [
2333+
{
2334+
type: 'Field',
2335+
field: 'Id',
2336+
},
22782337
],
22792338
sObject: 'Account',
2339+
where: {
2340+
left: {
2341+
field: 'Id',
2342+
literalType: 'APEX_BIND_VARIABLE',
2343+
operator: 'IN',
2344+
value: 'new Map<Id, SObject>(someVar).getSomeClass().records',
2345+
},
2346+
},
22802347
},
22812348
},
22822349
];

test/test.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const replacements = [{ matching: / last /i, replace: ' LAST ' }];
1212
// Uncomment these to easily test one specific query - useful for troubleshooting/bugfixing
1313

1414
// describe.only('parse queries', () => {
15-
// const testCase = testCases.find(tc => tc.testCase === 109);
15+
// const testCase = testCases.find(tc => tc.testCase === 62);
1616
// it(`should correctly parse test case ${testCase.testCase} - ${testCase.soql}`, () => {
1717
// const soqlQuery = parseQuery(testCase.soql, testCase.options);
1818
// console.log(soqlQuery);

0 commit comments

Comments
 (0)