Skip to content

Commit 47399a7

Browse files
authored
Merge pull request #156 from paustint/feature-155
Improve apex bind variable parsing #155
2 parents d4afaab + d1da4ee commit 47399a7

File tree

8 files changed

+308
-6
lines changed

8 files changed

+308
-6
lines changed

CHANGELOG.md

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

3-
## 4.1.0
3+
## 4.2.0
4+
5+
June 8, 2021
6+
7+
#155 - Apex bind variable support is improved to allow parsing of more complex Apex.
8+
9+
Review test cases 112 - 117 for examples of supported apex bind variables.
10+
11+
## 4.1.1
412

513
June 6, 2021
614

src/models.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,47 @@ export interface ExpressionContext {
201201
}
202202

203203
export interface ApexBindVariableExpressionContext {
204+
apex: CstNode[];
205+
COLON: IToken[];
206+
DECIMAL?: IToken[];
207+
}
208+
209+
export interface ApexBindVariableIdentifierContext {
204210
Identifier: IToken[];
211+
apexBindVariableFunctionArrayAccessor?: CstNode[];
212+
}
213+
214+
export interface ApexBindVariableNewInstantiationContext {
215+
new: IToken[];
216+
function: IToken[];
217+
apexBindVariableGeneric?: CstNode[];
218+
apexBindVariableFunctionParams: CstNode[];
219+
apexBindVariableFunctionArrayAccessor?: CstNode[];
220+
}
221+
222+
export interface ApexBindVariableFunctionCallContext {
223+
function: IToken[];
224+
apexBindVariableFunctionParams: CstNode[];
225+
apexBindVariableFunctionArrayAccessor?: CstNode[];
226+
}
227+
228+
export interface ApexBindVariableGenericContext {
229+
COMMA: IToken[];
230+
GREATER_THAN: IToken[];
231+
LESS_THAN: IToken[];
232+
parameter: IToken[];
233+
}
234+
235+
export interface ApexBindVariableFunctionParamsContext {
236+
L_PAREN: IToken[];
237+
R_PAREN: IToken[];
238+
parameter?: IToken[];
239+
}
240+
241+
export interface ApexBindVariableFunctionArrayAccessorContext {
242+
L_SQUARE_BRACKET: IToken[];
243+
R_SQUARE_BRACKET: IToken[];
244+
value: IToken[];
205245
}
206246

207247
export interface ExpressionOperatorContext {

src/parser/lexer.ts

Lines changed: 6 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({
@@ -749,6 +750,8 @@ export const Comma = createToken({ name: 'COMMA', pattern: ',', categories: [Sym
749750
export const Asterisk = createToken({ name: 'ASTERISK', pattern: '*', categories: [SymbolIdentifier] });
750751
export const LParen = createToken({ name: 'L_PAREN', pattern: '(', categories: [SymbolIdentifier] });
751752
export const RParen = createToken({ name: 'R_PAREN', pattern: ')', categories: [SymbolIdentifier] });
753+
export const LSquareBracket = createToken({ name: 'L_SQUARE_BRACKET', pattern: '[', categories: [SymbolIdentifier] });
754+
export const RSquareBracket = createToken({ name: 'R_SQUARE_BRACKET', pattern: ']', categories: [SymbolIdentifier] });
752755
export const Plus = createToken({ name: 'PLUS', pattern: '+', categories: [SymbolIdentifier] });
753756
export const Minus = createToken({ name: 'MINUS', pattern: '-', categories: [SymbolIdentifier] });
754757

@@ -913,6 +916,7 @@ export const allTokens = [
913916

914917
AboveOrBelow,
915918
Above,
919+
ApexNew,
916920
At,
917921
Below,
918922
DataCategory,
@@ -1035,6 +1039,8 @@ export const allTokens = [
10351039
Asterisk,
10361040
LParen,
10371041
RParen,
1042+
LSquareBracket,
1043+
RSquareBracket,
10381044
Plus,
10391045
Minus,
10401046
];

src/parser/parser.ts

Lines changed: 73 additions & 0 deletions
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,79 @@ export class SoqlParser extends CstParser {
617618

618619
private apexBindVariableExpression = this.RULE('apexBindVariableExpression', () => {
619620
this.CONSUME(lexer.Colon);
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.SUBRULE(this.apexBindVariableIdentifier, { LABEL: 'apex' }) },
637+
]),
638+
);
639+
},
640+
});
641+
});
642+
643+
private apexBindVariableIdentifier = this.RULE('apexBindVariableIdentifier', () => {
620644
this.CONSUME(lexer.Identifier);
645+
this.OPTION(() => this.SUBRULE(this.apexBindVariableFunctionArrayAccessor));
646+
});
647+
648+
private apexBindVariableNewInstantiation = this.RULE('apexBindVariableNewInstantiation', () => {
649+
this.CONSUME(lexer.ApexNew, { LABEL: 'new' });
650+
this.CONSUME(lexer.Identifier, { LABEL: 'function' });
651+
this.OPTION(() => {
652+
this.SUBRULE(this.apexBindVariableGeneric);
653+
});
654+
this.SUBRULE(this.apexBindVariableFunctionParams);
655+
this.OPTION1(() => this.SUBRULE(this.apexBindVariableFunctionArrayAccessor));
656+
});
657+
658+
private apexBindVariableFunctionCall = this.RULE('apexBindVariableFunctionCall', () => {
659+
this.CONSUME(lexer.Identifier, { LABEL: 'function' });
660+
this.SUBRULE(this.apexBindVariableFunctionParams);
661+
this.OPTION(() => this.SUBRULE(this.apexBindVariableFunctionArrayAccessor));
662+
});
663+
664+
private apexBindVariableGeneric = this.RULE('apexBindVariableGeneric', () => {
665+
this.CONSUME(lexer.LessThan);
666+
this.AT_LEAST_ONE_SEP({
667+
SEP: lexer.Comma,
668+
DEF: () => {
669+
this.CONSUME(lexer.Identifier, { LABEL: 'parameter' });
670+
},
671+
});
672+
this.CONSUME(lexer.GreaterThan);
673+
});
674+
675+
private apexBindVariableFunctionParams = this.RULE('apexBindVariableFunctionParams', () => {
676+
this.CONSUME(lexer.LParen);
677+
this.MANY_SEP({
678+
SEP: lexer.Comma,
679+
DEF: () => {
680+
this.CONSUME(lexer.Identifier, { LABEL: 'parameter' });
681+
},
682+
});
683+
this.CONSUME(lexer.RParen);
684+
});
685+
686+
// foo[3] or foo[somIntVariable]
687+
private apexBindVariableFunctionArrayAccessor = this.RULE('apexBindVariableFunctionArrayAccessor', () => {
688+
this.CONSUME(lexer.LSquareBracket);
689+
this.OR([
690+
{ ALT: () => this.CONSUME(lexer.UnsignedInteger, { LABEL: 'value' }) },
691+
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'value' }) },
692+
]);
693+
this.CONSUME(lexer.RSquareBracket);
621694
});
622695

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

src/parser/visitor.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import {
3434
} from '../api/api-models';
3535
import {
3636
ApexBindVariableExpressionContext,
37+
ApexBindVariableFunctionArrayAccessorContext,
38+
ApexBindVariableFunctionCallContext,
39+
ApexBindVariableFunctionParamsContext,
40+
ApexBindVariableGenericContext,
41+
ApexBindVariableIdentifierContext,
42+
ApexBindVariableNewInstantiationContext,
3743
ArrayExpressionWithType,
3844
AtomicExpressionContext,
3945
BooleanContext,
@@ -791,7 +797,48 @@ class SOQLVisitor extends BaseSoqlVisitor {
791797
}
792798

793799
apexBindVariableExpression(ctx: ApexBindVariableExpressionContext): string {
794-
return ctx.Identifier[0].image;
800+
return ctx.apex.map(item => this.visit(item)).join('.');
801+
}
802+
803+
apexBindVariableIdentifier(ctx: ApexBindVariableIdentifierContext): string {
804+
let output = ctx.Identifier[0].image;
805+
if (ctx.apexBindVariableFunctionArrayAccessor) {
806+
output += this.visit(ctx.apexBindVariableFunctionArrayAccessor[0]);
807+
}
808+
return output;
809+
}
810+
811+
apexBindVariableNewInstantiation(ctx: ApexBindVariableNewInstantiationContext): string {
812+
let output = `new ${ctx.function[0].image}`;
813+
if (ctx.apexBindVariableGeneric) {
814+
output += this.visit(ctx.apexBindVariableGeneric[0]);
815+
}
816+
output += this.visit(ctx.apexBindVariableFunctionParams[0]);
817+
if (ctx.apexBindVariableFunctionArrayAccessor) {
818+
output += this.visit(ctx.apexBindVariableFunctionArrayAccessor[0]);
819+
}
820+
return output;
821+
}
822+
823+
apexBindVariableFunctionCall(ctx: ApexBindVariableFunctionCallContext): string {
824+
let output = `${ctx.function[0].image}${this.visit(ctx.apexBindVariableFunctionParams[0])}`;
825+
if (ctx.apexBindVariableFunctionArrayAccessor) {
826+
output += this.visit(ctx.apexBindVariableFunctionArrayAccessor[0]);
827+
}
828+
return output;
829+
}
830+
831+
apexBindVariableGeneric(ctx: ApexBindVariableGenericContext): string {
832+
return `<${ctx.parameter.map(item => item.image).join(', ')}>`;
833+
}
834+
835+
apexBindVariableFunctionParams(ctx: ApexBindVariableFunctionParamsContext): string {
836+
const params = Array.isArray(ctx.parameter) ? ctx.parameter : [];
837+
return `(${params.map(item => item.image).join(', ')})`;
838+
}
839+
840+
apexBindVariableFunctionArrayAccessor(ctx: ApexBindVariableFunctionArrayAccessorContext): string {
841+
return `[${ctx.value[0].image}]`;
795842
}
796843

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

test/test-cases-for-is-valid.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,5 +466,22 @@ export const testCases: TestCaseForFormat[] = [
466466
soql: `SELECT Id FROM Account WHERE NOT Name = '2'`,
467467
isValid: true,
468468
},
469+
{
470+
testCase: 158,
471+
options: { allowApexBindVariables: true },
472+
soql: `
473+
SELECT Id
474+
FROM Account
475+
WHERE
476+
Id IN :new Map<
477+
Id,
478+
SObject
479+
>
480+
(someVar)
481+
.getSomeClass()
482+
.records
483+
`,
484+
isValid: true,
485+
},
469486
];
470487
export default testCases;

0 commit comments

Comments
 (0)