Skip to content

Commit 9553619

Browse files
committed
Fix incorrect parsing for NOT negation
Ensure that all `expressionNegation` entries are visited instead of just the first one in the case where there aer multiple negations in a row Removed `NOT IN` keyword in lexer to avoid false positives for identifiers (fields) that start with `In` preceded by `NOT` Enable typescript strict mode to help enforce handling data properly - this required many minor updates. Improved visitor types to better express actual shape of context
1 parent c1f7ad5 commit 9553619

File tree

15 files changed

+276
-87
lines changed

15 files changed

+276
-87
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 4.10.0
4+
5+
Jan 13, 2024
6+
7+
- Fixed where clause's that have a field name beginning with `In` preceded by the `NOT` operator. These were parsed as `NOT IN` instead of `NOT` followed by a field name, example: `NOT Invoice__c`
8+
- https://github.com/jetstreamapp/jetstream/issues/702
9+
- Fixed queries that have two consecutive `NOT` operators (#237)
10+
- Enabled Typescript strict mode and made a number of minor fixes related to this.
11+
- When using `getField` which return `FieldFunctionExpression` will now always return an empty array even if no parameters are provided.
12+
313
## 4.9.2
414

515
July 24, 2023

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/api-models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export interface Subquery extends QueryBase {
157157
export type WhereClause = WhereClauseWithoutOperator | WhereClauseWithRightCondition;
158158

159159
export interface WhereClauseWithoutOperator {
160-
left: ConditionWithValueQuery;
160+
left: ConditionWithValueQuery | null;
161161
}
162162

163163
export interface WhereClauseWithRightCondition extends WhereClauseWithoutOperator {

src/api/public-utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ export function getField(input: string | ComposeFieldInput): SoqlModels.FieldTyp
100100
field: input,
101101
};
102102
} else if (isComposeFieldFunction(input)) {
103-
let parameters: string[] | SoqlModels.FieldFunctionExpression[];
103+
let parameters: string[] | SoqlModels.FieldFunctionExpression[] = [];
104104
if (input.parameters) {
105105
parameters = (Array.isArray(input.parameters) ? input.parameters : [input.parameters]) as
106106
| string[]
107107
| SoqlModels.FieldFunctionExpression[];
108108
}
109109
return {
110110
type: 'FieldFunctionExpression',
111-
functionName: input.functionName || input.fn,
111+
functionName: (input.functionName || input.fn)!,
112112
parameters,
113113
alias: input.alias,
114114
};
@@ -122,7 +122,7 @@ export function getField(input: string | ComposeFieldInput): SoqlModels.FieldTyp
122122
} else if (isComposeFieldSubquery(input)) {
123123
return {
124124
type: 'FieldSubquery',
125-
subquery: input.subquery,
125+
subquery: input.subquery!,
126126
};
127127
} else if (isComposeFieldTypeof(input)) {
128128
return {
@@ -259,7 +259,7 @@ export function getFlattenedFields(
259259
break;
260260
}
261261
})
262-
.filter(field => isString(field));
262+
.filter(field => isString(field)) as string[];
263263

264264
return parsedFields;
265265
}

src/composer/composer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export class Compose {
7676
constructor(private soql: Query, config: Partial<SoqlComposeConfig> = {}) {
7777
config = { autoCompose: true, ...config };
7878
const { logging, format } = config;
79-
this.logging = logging;
80-
this.format = format;
79+
this.logging = !!logging;
80+
this.format = !!format;
8181
this.query = '';
8282

8383
this.formatter = new Formatter(this.format, {
@@ -126,7 +126,7 @@ export class Compose {
126126
output += ` ${fn.alias}`;
127127
}
128128

129-
return output;
129+
return output!;
130130
}
131131

132132
/**
@@ -262,7 +262,7 @@ export class Compose {
262262
public parseFields(fields: FieldType[]): { text: string; typeOfClause?: string[] }[] {
263263
return fields.map(field => {
264264
let text = '';
265-
let typeOfClause: string[];
265+
let typeOfClause: string[] | undefined;
266266

267267
const objPrefix = (field as any).objectPrefix ? `${(field as any).objectPrefix}.` : '';
268268
switch (field.type) {

src/formatter/formatter.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ export interface FieldData {
1616
export interface FormatOptions {
1717
/**
1818
* @default 1
19+
*
1920
* Number of tabs to indent by
2021
* These defaults to one
2122
*/
2223
numIndent?: number;
2324
/**
2425
* @default 60
26+
*
2527
* Number of characters before wrapping to a new line.
2628
* Set to 0 or 1 to force every field on a new line.
2729
* TYPEOF fields do not honor this setting, they will always be on one line unless `newLineAfterKeywords` is true,
@@ -30,6 +32,7 @@ export interface FormatOptions {
3032
fieldMaxLineLength?: number;
3133
/**
3234
* @default true
35+
*
3336
* Set to true to have a subquery parentheses start and end on a new line.
3437
* This will be set to true if `newLineAfterKeywords` is true, in which case this property can be omitted
3538
*/
@@ -38,6 +41,7 @@ export interface FormatOptions {
3841
whereClauseOperatorsIndented?: boolean;
3942
/**
4043
* @default false
44+
*
4145
* Adds a new line and indent after all keywords (such as SELECT, FROM, WHERE, ORDER BY, etc..)
4246
* Setting this to true will add new lines in other places as well, such as complex WHERE clauses
4347
*/
@@ -52,18 +56,18 @@ export interface FormatOptions {
5256
*/
5357
export class Formatter {
5458
enabled: boolean;
55-
private options: FormatOptions;
59+
private options: Required<FormatOptions>;
5660
private currIndent = 1;
5761

5862
constructor(enabled: boolean, options: FormatOptions) {
5963
this.enabled = enabled;
6064
this.options = {
61-
numIndent: 1,
62-
fieldMaxLineLength: 60,
63-
fieldSubqueryParensOnOwnLine: true,
64-
newLineAfterKeywords: false,
65-
logging: false,
66-
...options,
65+
numIndent: options.numIndent ?? 1,
66+
fieldMaxLineLength: options.fieldMaxLineLength ?? 60,
67+
fieldSubqueryParensOnOwnLine: options.fieldSubqueryParensOnOwnLine ?? true,
68+
whereClauseOperatorsIndented: true,
69+
newLineAfterKeywords: options.newLineAfterKeywords ?? false,
70+
logging: options.logging ?? false,
6771
};
6872
if (this.options.newLineAfterKeywords) {
6973
this.options.fieldSubqueryParensOnOwnLine = true;
@@ -77,7 +81,7 @@ export class Formatter {
7781
}
7882

7983
private getIndent(additionalIndent = 0) {
80-
return this.repeatChar((this.currIndent + additionalIndent) * this.options.numIndent, '\t');
84+
return this.repeatChar((this.currIndent + additionalIndent) * (this.options.numIndent || 1), '\t');
8185
}
8286

8387
private repeatChar(numTimes: number, char: string) {
@@ -236,7 +240,7 @@ export class Formatter {
236240
groupBy.forEach((token, i) => {
237241
const nextToken = groupBy[i + 1];
238242
currLen += token.length;
239-
if (nextToken && (currLen + nextToken.length > this.options.fieldMaxLineLength || this.options.newLineAfterKeywords)) {
243+
if (nextToken && (currLen + nextToken.length > (this.options.fieldMaxLineLength || 0) || this.options.newLineAfterKeywords)) {
240244
output += `${token},\n\t`;
241245
currLen = 0;
242246
} else {
@@ -256,7 +260,7 @@ export class Formatter {
256260
* @param [leadingParenInline] Make the leading paren inline (last for "(" and first for ")") used for negation condition
257261
* @returns
258262
*/
259-
formatParens(count: number, character: '(' | ')', leadingParenInline = false) {
263+
formatParens(count: number | undefined, character: '(' | ')', leadingParenInline = false) {
260264
let output = '';
261265
if (isNumber(count) && count > 0) {
262266
if (this.enabled) {

src/models.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ExpressionTree<T> {
1818
*/
1919

2020
interface WithIdentifier {
21-
Identifier?: IToken[];
21+
Identifier: IToken[];
2222
}
2323

2424
export interface SelectStatementContext {
@@ -61,7 +61,7 @@ export interface SelectClauseFunctionIdentifierContext extends WithIdentifier {
6161
}
6262

6363
export interface SelectClauseSubqueryIdentifierContext extends WithIdentifier {
64-
selectStatement?: CstNode[];
64+
selectStatement: CstNode[];
6565
}
6666

6767
export interface SelectClauseTypeOfContext extends WithIdentifier {
@@ -102,10 +102,24 @@ export interface ConditionExpressionContext {
102102
expression: CstNode[];
103103
}
104104

105-
export interface WithClauseContext {
106-
withSecurityEnforced?: CstNode[];
107-
withAccessLevel?: IToken[];
108-
withDataCategory?: CstNode[];
105+
export type WithClauseContext = WithSecurityEnforcedClauseContext | WithAccessLevelClauseContext | WithDataCategoryClauseContext;
106+
107+
export interface WithSecurityEnforcedClauseContext {
108+
withSecurityEnforced: CstNode[];
109+
withAccessLevel?: never;
110+
withDataCategory?: never;
111+
}
112+
113+
export interface WithAccessLevelClauseContext {
114+
withSecurityEnforced?: never;
115+
withAccessLevel: IToken[];
116+
withDataCategory?: never;
117+
}
118+
119+
export interface WithDataCategoryClauseContext {
120+
withSecurityEnforced?: never;
121+
withAccessLevel?: never;
122+
withDataCategory: CstNode[];
109123
}
110124

111125
export interface WithDateCategoryContext {
@@ -163,6 +177,17 @@ export interface OperatorContext {
163177
operator: IToken[];
164178
}
165179

180+
export type OperatorOrNotInContext = OperatorWithoutNotInContext | OperatorNotInContext;
181+
182+
export interface OperatorWithoutNotInContext extends OperatorContext {
183+
notIn?: never;
184+
}
185+
186+
export interface OperatorNotInContext {
187+
operator?: never;
188+
notIn: CstNode[];
189+
}
190+
166191
export interface BooleanContext {
167192
boolean: IToken[];
168193
}
@@ -249,10 +274,22 @@ export interface ApexBindVariableFunctionArrayAccessorContext {
249274
value: IToken[];
250275
}
251276

252-
export interface ExpressionOperatorContext {
277+
export type ExpressionOperatorContext = ExpressionOperatorRhsContext & ExpressionWithRelationalOrSetOperatorContext;
278+
279+
export interface ExpressionOperatorRhsContext {
253280
rhs: CstNode[];
254-
relationalOperator?: CstNode[];
255-
setOperator?: CstNode[];
281+
}
282+
283+
type ExpressionWithRelationalOrSetOperatorContext = ExpressionWithRelationalOperatorContext | ExpressionWithSetOperatorOperatorContext;
284+
285+
export interface ExpressionWithRelationalOperatorContext {
286+
relationalOperator: CstNode[];
287+
setOperator?: never;
288+
}
289+
290+
export interface ExpressionWithSetOperatorOperatorContext {
291+
relationalOperator?: never;
292+
setOperator: CstNode[];
256293
}
257294

258295
export interface FromClauseContext extends WithIdentifier {

src/parser/lexer.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,6 @@ export const GroupBy = createToken({ name: 'GROUP_BY', pattern: /GROUP BY/i, lon
230230
export const Having = createToken({ name: 'HAVING', pattern: /HAVING/i, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
231231
export const In = createToken({ name: 'IN', pattern: /IN/i, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
232232

233-
// FIXME: split into two tokens, NOT is a reserved keyword, IN is not
234-
export const NotIn = createToken({ name: 'NOT_IN', pattern: /NOT IN/i, longer_alt: Identifier });
235-
236233
export const Includes = createToken({
237234
name: 'INCLUDES',
238235
pattern: /INCLUDES/i,
@@ -1025,7 +1022,6 @@ export const allTokens = [
10251022
Standard,
10261023

10271024
In,
1028-
NotIn,
10291025
For,
10301026
Or,
10311027
Last,

src/parser/parser.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ export class SoqlParser extends CstParser {
4545
public allowApexBindVariables = false;
4646
public ignoreParseErrors = false;
4747

48-
constructor({ ignoreParseErrors }: { ignoreParseErrors: boolean } = { ignoreParseErrors: false }) {
48+
constructor({ ignoreParseErrors }: { ignoreParseErrors?: boolean | null } = { ignoreParseErrors: false }) {
4949
super(lexer.allTokens, {
5050
// true in production (webpack replaces this string)
5151
skipValidations: false,
52-
recoveryEnabled: ignoreParseErrors,
52+
recoveryEnabled: !!ignoreParseErrors,
5353
// nodeLocationTracking: 'full', // not sure if needed, could look at
5454
});
55-
this.ignoreParseErrors = ignoreParseErrors;
55+
this.ignoreParseErrors = !!ignoreParseErrors;
5656
this.performSelfAnalysis();
5757
}
5858

@@ -157,7 +157,7 @@ export class SoqlParser extends CstParser {
157157
this.$_selectClauseFunctionIdentifier ||
158158
(this.$_selectClauseFunctionIdentifier = [
159159
{ ALT: () => this.SUBRULE(this.dateFunction, { LABEL: 'fn' }) },
160-
{ ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'fn', ARGS: [true] }) },
160+
{ ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'fn' }) },
161161
{ ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'fn' }) },
162162
{ ALT: () => this.SUBRULE(this.fieldsFunction, { LABEL: 'fn' }) },
163163
{ ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'fn' }) },
@@ -261,7 +261,7 @@ export class SoqlParser extends CstParser {
261261
const parenCount = this.getParenCount();
262262
this.AT_LEAST_ONE({
263263
DEF: () => {
264-
this.SUBRULE(this.conditionExpression, { ARGS: [parenCount, false, true, true] });
264+
this.SUBRULE(this.conditionExpression, { ARGS: [parenCount, true, true] });
265265
},
266266
});
267267

@@ -280,7 +280,7 @@ export class SoqlParser extends CstParser {
280280

281281
private conditionExpression = this.RULE(
282282
'conditionExpression',
283-
(parenCount?: ParenCount, allowSubquery?: boolean, allowAggregateFn?: boolean, allowLocationFn?: boolean) => {
283+
(parenCount?: ParenCount, allowAggregateFn?: boolean, allowLocationFn?: boolean) => {
284284
// argument is undefined during self-analysis, need to initialize to avoid exception
285285
parenCount = this.getParenCount(parenCount);
286286
this.OPTION(() => {
@@ -292,15 +292,15 @@ export class SoqlParser extends CstParser {
292292

293293
// MAX_LOOKAHEAD -> this is increased because an arbitrary number of parens could be used causing a parsing error
294294
// this does not allow infinite parentheses, but is more than enough for any real use-cases
295-
// Under no circumstances would large numbers of nested expressions not be expressable with fewer conditions
295+
// Under no circumstances would large numbers of nested expressions not be expressible with fewer conditions
296296
this.MANY({
297297
MAX_LOOKAHEAD: 10,
298298
DEF: () => this.SUBRULE(this.expressionPartWithNegation, { ARGS: [parenCount], LABEL: 'expressionNegation' }),
299299
});
300300

301301
this.OR1({
302302
MAX_LOOKAHEAD: 10,
303-
DEF: [{ ALT: () => this.SUBRULE(this.expression, { ARGS: [parenCount, allowSubquery, allowAggregateFn, allowLocationFn] }) }],
303+
DEF: [{ ALT: () => this.SUBRULE(this.expression, { ARGS: [parenCount, false, allowAggregateFn, allowLocationFn] }) }],
304304
});
305305
},
306306
);
@@ -372,7 +372,7 @@ export class SoqlParser extends CstParser {
372372
const parenCount = this.getParenCount();
373373
this.AT_LEAST_ONE({
374374
DEF: () => {
375-
this.SUBRULE(this.conditionExpression, { ARGS: [parenCount, true, undefined, undefined] });
375+
this.SUBRULE(this.conditionExpression, { ARGS: [parenCount, true, false] });
376376
},
377377
});
378378

@@ -575,8 +575,8 @@ export class SoqlParser extends CstParser {
575575
});
576576

577577
this.OR1([
578-
{ GATE: () => allowAggregateFn, ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'lhs' }) },
579-
{ GATE: () => allowLocationFn, ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'lhs' }) },
578+
{ GATE: () => !!allowAggregateFn, ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'lhs' }) },
579+
{ GATE: () => !!allowLocationFn, ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'lhs' }) },
580580
{ ALT: () => this.SUBRULE(this.dateFunction, { LABEL: 'lhs' }) },
581581
{ ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'lhs' }) },
582582
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'lhs' }) },
@@ -757,12 +757,17 @@ export class SoqlParser extends CstParser {
757757
private setOperator = this.RULE('setOperator', () => {
758758
this.OR([
759759
{ ALT: () => this.CONSUME(lexer.In, { LABEL: 'operator' }) },
760-
{ ALT: () => this.CONSUME(lexer.NotIn, { LABEL: 'operator' }) },
760+
{ ALT: () => this.SUBRULE(this.notInOperator, { LABEL: 'notIn' }) },
761761
{ ALT: () => this.CONSUME(lexer.Includes, { LABEL: 'operator' }) },
762762
{ ALT: () => this.CONSUME(lexer.Excludes, { LABEL: 'operator' }) },
763763
]);
764764
});
765765

766+
private notInOperator = this.RULE('notInOperator', () => {
767+
this.CONSUME(lexer.Not, { LABEL: 'operator' });
768+
this.CONSUME(lexer.In, { LABEL: 'operator' });
769+
});
770+
766771
private booleanValue = this.RULE('booleanValue', () => {
767772
this.OR([
768773
{ ALT: () => this.CONSUME(lexer.True, { LABEL: 'boolean' }) },

0 commit comments

Comments
 (0)