flowquery 1.0.27 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +5 -1
- package/dist/graph/relationship.js.map +1 -1
- package/dist/parsing/base_parser.d.ts +1 -1
- package/dist/parsing/base_parser.d.ts.map +1 -1
- package/dist/parsing/base_parser.js.map +1 -1
- package/dist/parsing/expressions/operator.d.ts +37 -1
- package/dist/parsing/expressions/operator.d.ts.map +1 -1
- package/dist/parsing/expressions/operator.js +121 -2
- package/dist/parsing/expressions/operator.js.map +1 -1
- package/dist/parsing/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/string_distance.d.ts +7 -0
- package/dist/parsing/functions/string_distance.d.ts.map +1 -0
- package/dist/parsing/functions/string_distance.js +84 -0
- package/dist/parsing/functions/string_distance.js.map +1 -0
- package/dist/parsing/parser.d.ts +6 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +109 -11
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/keyword.d.ts +4 -1
- package/dist/tokenization/keyword.d.ts.map +1 -1
- package/dist/tokenization/keyword.js +3 -0
- package/dist/tokenization/keyword.js.map +1 -1
- package/dist/tokenization/token.d.ts +6 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +18 -0
- package/dist/tokenization/token.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/relationship.py +5 -1
- package/flowquery-py/src/parsing/expressions/__init__.py +4 -0
- package/flowquery-py/src/parsing/expressions/operator.py +102 -0
- package/flowquery-py/src/parsing/functions/__init__.py +2 -0
- package/flowquery-py/src/parsing/functions/string_distance.py +88 -0
- package/flowquery-py/src/parsing/parser.py +111 -14
- package/flowquery-py/src/tokenization/keyword.py +3 -0
- package/flowquery-py/src/tokenization/token.py +21 -0
- package/flowquery-py/tests/compute/test_runner.py +366 -1
- package/flowquery-py/tests/parsing/test_expression.py +121 -1
- package/flowquery-py/tests/parsing/test_parser.py +203 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/relationship.ts +4 -1
- package/src/parsing/base_parser.ts +1 -1
- package/src/parsing/expressions/operator.ts +129 -1
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/string_distance.ts +80 -0
- package/src/parsing/parser.ts +120 -10
- package/src/tokenization/keyword.ts +3 -0
- package/src/tokenization/token.ts +24 -0
- package/tests/compute/runner.test.ts +333 -0
- package/tests/parsing/expression.test.ts +150 -16
- package/tests/parsing/parser.test.ts +200 -0
package/package.json
CHANGED
|
@@ -133,7 +133,7 @@ class Relationship extends ASTNode {
|
|
|
133
133
|
const followId = isLeft ? "left_id" : "right_id";
|
|
134
134
|
while (findMatch(left_id, hop)) {
|
|
135
135
|
const data: RelationshipRecord = this._data?.current(hop) as RelationshipRecord;
|
|
136
|
-
if (hop >= this.hops!.min) {
|
|
136
|
+
if (hop + 1 >= this.hops!.min) {
|
|
137
137
|
this.setValue(this);
|
|
138
138
|
if (!this._matchesProperties(hop)) {
|
|
139
139
|
continue;
|
|
@@ -146,6 +146,9 @@ class Relationship extends ASTNode {
|
|
|
146
146
|
await this.find(data[followId], hop + 1);
|
|
147
147
|
}
|
|
148
148
|
this._matches.pop();
|
|
149
|
+
} else {
|
|
150
|
+
// Below minimum hops: traverse the edge without yielding a match
|
|
151
|
+
await this.find(data[followId], hop + 1);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
// Restore original source node
|
|
@@ -9,7 +9,7 @@ import Tokenizer from "../tokenization/tokenizer";
|
|
|
9
9
|
*/
|
|
10
10
|
class BaseParser {
|
|
11
11
|
private tokens: Token[] = <Token[]>[];
|
|
12
|
-
|
|
12
|
+
protected tokenIndex: number = 0;
|
|
13
13
|
|
|
14
14
|
constructor(tokens: Token[] | null = null) {
|
|
15
15
|
if (tokens !== null) {
|
|
@@ -172,7 +172,126 @@ class Is extends Operator {
|
|
|
172
172
|
super(-1, true);
|
|
173
173
|
}
|
|
174
174
|
public value(): number {
|
|
175
|
-
return this.lhs.value()
|
|
175
|
+
return this.lhs.value() == this.rhs.value() ? 1 : 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class IsNot extends Operator {
|
|
180
|
+
constructor() {
|
|
181
|
+
super(-1, true);
|
|
182
|
+
}
|
|
183
|
+
public value(): number {
|
|
184
|
+
return this.lhs.value() != this.rhs.value() ? 1 : 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class In extends Operator {
|
|
189
|
+
constructor() {
|
|
190
|
+
super(-1, true);
|
|
191
|
+
}
|
|
192
|
+
public value(): number {
|
|
193
|
+
const list = this.rhs.value();
|
|
194
|
+
if (!Array.isArray(list)) {
|
|
195
|
+
throw new Error("Right operand of IN must be a list");
|
|
196
|
+
}
|
|
197
|
+
return list.includes(this.lhs.value()) ? 1 : 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class NotIn extends Operator {
|
|
202
|
+
constructor() {
|
|
203
|
+
super(-1, true);
|
|
204
|
+
}
|
|
205
|
+
public value(): number {
|
|
206
|
+
const list = this.rhs.value();
|
|
207
|
+
if (!Array.isArray(list)) {
|
|
208
|
+
throw new Error("Right operand of NOT IN must be a list");
|
|
209
|
+
}
|
|
210
|
+
return list.includes(this.lhs.value()) ? 0 : 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class Contains extends Operator {
|
|
215
|
+
constructor() {
|
|
216
|
+
super(0, true);
|
|
217
|
+
}
|
|
218
|
+
public value(): number {
|
|
219
|
+
const str = this.lhs.value();
|
|
220
|
+
const search = this.rhs.value();
|
|
221
|
+
if (typeof str !== "string" || typeof search !== "string") {
|
|
222
|
+
throw new Error("CONTAINS requires string operands");
|
|
223
|
+
}
|
|
224
|
+
return str.includes(search) ? 1 : 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class NotContains extends Operator {
|
|
229
|
+
constructor() {
|
|
230
|
+
super(0, true);
|
|
231
|
+
}
|
|
232
|
+
public value(): number {
|
|
233
|
+
const str = this.lhs.value();
|
|
234
|
+
const search = this.rhs.value();
|
|
235
|
+
if (typeof str !== "string" || typeof search !== "string") {
|
|
236
|
+
throw new Error("NOT CONTAINS requires string operands");
|
|
237
|
+
}
|
|
238
|
+
return str.includes(search) ? 0 : 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class StartsWith extends Operator {
|
|
243
|
+
constructor() {
|
|
244
|
+
super(0, true);
|
|
245
|
+
}
|
|
246
|
+
public value(): number {
|
|
247
|
+
const str = this.lhs.value();
|
|
248
|
+
const search = this.rhs.value();
|
|
249
|
+
if (typeof str !== "string" || typeof search !== "string") {
|
|
250
|
+
throw new Error("STARTS WITH requires string operands");
|
|
251
|
+
}
|
|
252
|
+
return str.startsWith(search) ? 1 : 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
class NotStartsWith extends Operator {
|
|
257
|
+
constructor() {
|
|
258
|
+
super(0, true);
|
|
259
|
+
}
|
|
260
|
+
public value(): number {
|
|
261
|
+
const str = this.lhs.value();
|
|
262
|
+
const search = this.rhs.value();
|
|
263
|
+
if (typeof str !== "string" || typeof search !== "string") {
|
|
264
|
+
throw new Error("NOT STARTS WITH requires string operands");
|
|
265
|
+
}
|
|
266
|
+
return str.startsWith(search) ? 0 : 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
class EndsWith extends Operator {
|
|
271
|
+
constructor() {
|
|
272
|
+
super(0, true);
|
|
273
|
+
}
|
|
274
|
+
public value(): number {
|
|
275
|
+
const str = this.lhs.value();
|
|
276
|
+
const search = this.rhs.value();
|
|
277
|
+
if (typeof str !== "string" || typeof search !== "string") {
|
|
278
|
+
throw new Error("ENDS WITH requires string operands");
|
|
279
|
+
}
|
|
280
|
+
return str.endsWith(search) ? 1 : 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
class NotEndsWith extends Operator {
|
|
285
|
+
constructor() {
|
|
286
|
+
super(0, true);
|
|
287
|
+
}
|
|
288
|
+
public value(): number {
|
|
289
|
+
const str = this.lhs.value();
|
|
290
|
+
const search = this.rhs.value();
|
|
291
|
+
if (typeof str !== "string" || typeof search !== "string") {
|
|
292
|
+
throw new Error("NOT ENDS WITH requires string operands");
|
|
293
|
+
}
|
|
294
|
+
return str.endsWith(search) ? 0 : 1;
|
|
176
295
|
}
|
|
177
296
|
}
|
|
178
297
|
|
|
@@ -194,4 +313,13 @@ export {
|
|
|
194
313
|
Or,
|
|
195
314
|
Not,
|
|
196
315
|
Is,
|
|
316
|
+
IsNot,
|
|
317
|
+
In,
|
|
318
|
+
NotIn,
|
|
319
|
+
Contains,
|
|
320
|
+
NotContains,
|
|
321
|
+
StartsWith,
|
|
322
|
+
NotStartsWith,
|
|
323
|
+
EndsWith,
|
|
324
|
+
NotEndsWith,
|
|
197
325
|
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import Function from "./function";
|
|
2
|
+
import { FunctionDef } from "./function_metadata";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Computes the normalized Levenshtein distance between two strings.
|
|
6
|
+
* The Levenshtein distance is the minimum number of single-character edits
|
|
7
|
+
* (insertions, deletions, or substitutions) required to change one string into the other.
|
|
8
|
+
* The result is normalized to [0, 1] by dividing by the length of the longer string.
|
|
9
|
+
*
|
|
10
|
+
* @param a - First string
|
|
11
|
+
* @param b - Second string
|
|
12
|
+
* @returns The normalized Levenshtein distance (0 = identical, 1 = completely different)
|
|
13
|
+
*/
|
|
14
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
15
|
+
const m = a.length;
|
|
16
|
+
const n = b.length;
|
|
17
|
+
|
|
18
|
+
// Both empty strings are identical
|
|
19
|
+
if (m === 0 && n === 0) return 0;
|
|
20
|
+
|
|
21
|
+
// Create a matrix of size (m+1) x (n+1)
|
|
22
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
23
|
+
|
|
24
|
+
// Base cases: transforming empty string to/from a prefix
|
|
25
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
26
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
27
|
+
|
|
28
|
+
// Fill in the rest of the matrix
|
|
29
|
+
for (let i = 1; i <= m; i++) {
|
|
30
|
+
for (let j = 1; j <= n; j++) {
|
|
31
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
32
|
+
dp[i][j] = Math.min(
|
|
33
|
+
dp[i - 1][j] + 1, // deletion
|
|
34
|
+
dp[i][j - 1] + 1, // insertion
|
|
35
|
+
dp[i - 1][j - 1] + cost // substitution
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Normalize by the length of the longer string
|
|
41
|
+
return dp[m][n] / Math.max(m, n);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@FunctionDef({
|
|
45
|
+
description:
|
|
46
|
+
"Computes the normalized Levenshtein distance between two strings. Returns a value in [0, 1] where 0 means identical and 1 means completely different.",
|
|
47
|
+
category: "scalar",
|
|
48
|
+
parameters: [
|
|
49
|
+
{ name: "string1", description: "First string", type: "string" },
|
|
50
|
+
{ name: "string2", description: "Second string", type: "string" },
|
|
51
|
+
],
|
|
52
|
+
output: {
|
|
53
|
+
description: "Normalized Levenshtein distance (0 = identical, 1 = completely different)",
|
|
54
|
+
type: "number",
|
|
55
|
+
example: 0.43,
|
|
56
|
+
},
|
|
57
|
+
examples: [
|
|
58
|
+
"RETURN string_distance('kitten', 'sitting')",
|
|
59
|
+
"WITH 'hello' AS a, 'hallo' AS b RETURN string_distance(a, b)",
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
class StringDistance extends Function {
|
|
63
|
+
constructor() {
|
|
64
|
+
super("string_distance");
|
|
65
|
+
this._expectedParameterCount = 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public value(): any {
|
|
69
|
+
const str1 = this.getChildren()[0].value();
|
|
70
|
+
const str2 = this.getChildren()[1].value();
|
|
71
|
+
if (typeof str1 !== "string" || typeof str2 !== "string") {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Invalid arguments for string_distance function: both arguments must be strings"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return levenshteinDistance(str1, str2);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default StringDistance;
|
package/src/parsing/parser.ts
CHANGED
|
@@ -24,7 +24,19 @@ import RangeLookup from "./data_structures/range_lookup";
|
|
|
24
24
|
import Expression from "./expressions/expression";
|
|
25
25
|
import FString from "./expressions/f_string";
|
|
26
26
|
import Identifier from "./expressions/identifier";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
Contains,
|
|
29
|
+
EndsWith,
|
|
30
|
+
In,
|
|
31
|
+
Is,
|
|
32
|
+
IsNot,
|
|
33
|
+
Not,
|
|
34
|
+
NotContains,
|
|
35
|
+
NotEndsWith,
|
|
36
|
+
NotIn,
|
|
37
|
+
NotStartsWith,
|
|
38
|
+
StartsWith,
|
|
39
|
+
} from "./expressions/operator";
|
|
28
40
|
import Reference from "./expressions/reference";
|
|
29
41
|
import String from "./expressions/string";
|
|
30
42
|
import AggregateFunction from "./functions/aggregate_function";
|
|
@@ -434,10 +446,7 @@ class Parser extends BaseParser {
|
|
|
434
446
|
let node = new Node();
|
|
435
447
|
node.properties = new Map(this.parseProperties());
|
|
436
448
|
node.label = label!;
|
|
437
|
-
if (
|
|
438
|
-
node.identifier = identifier;
|
|
439
|
-
this.variables.set(identifier, node);
|
|
440
|
-
} else if (identifier !== null) {
|
|
449
|
+
if (identifier !== null && this.variables.has(identifier)) {
|
|
441
450
|
let reference = this.variables.get(identifier);
|
|
442
451
|
// Resolve through Expression -> Reference -> Node (e.g., after WITH)
|
|
443
452
|
if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
|
|
@@ -450,6 +459,9 @@ class Parser extends BaseParser {
|
|
|
450
459
|
throw new Error(`Undefined node reference: ${identifier}`);
|
|
451
460
|
}
|
|
452
461
|
node = new NodeReference(node, reference);
|
|
462
|
+
} else if (identifier !== null) {
|
|
463
|
+
node.identifier = identifier;
|
|
464
|
+
this.variables.set(identifier, node);
|
|
453
465
|
}
|
|
454
466
|
if (!this.token.isRightParenthesis()) {
|
|
455
467
|
throw new Error("Expected closing parenthesis for node definition");
|
|
@@ -632,10 +644,7 @@ class Parser extends BaseParser {
|
|
|
632
644
|
let relationship = new Relationship();
|
|
633
645
|
relationship.direction = direction;
|
|
634
646
|
relationship.properties = properties;
|
|
635
|
-
if (
|
|
636
|
-
relationship.identifier = variable;
|
|
637
|
-
this.variables.set(variable, relationship);
|
|
638
|
-
} else if (variable !== null) {
|
|
647
|
+
if (variable !== null && this.variables.has(variable)) {
|
|
639
648
|
let reference = this.variables.get(variable);
|
|
640
649
|
// Resolve through Expression -> Reference -> Relationship (e.g., after WITH)
|
|
641
650
|
if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
|
|
@@ -648,6 +657,9 @@ class Parser extends BaseParser {
|
|
|
648
657
|
throw new Error(`Undefined relationship reference: ${variable}`);
|
|
649
658
|
}
|
|
650
659
|
relationship = new RelationshipReference(relationship, reference);
|
|
660
|
+
} else if (variable !== null) {
|
|
661
|
+
relationship.identifier = variable;
|
|
662
|
+
this.variables.set(variable, relationship);
|
|
651
663
|
}
|
|
652
664
|
if (hops !== null) {
|
|
653
665
|
relationship.hops = hops;
|
|
@@ -853,7 +865,25 @@ class Parser extends BaseParser {
|
|
|
853
865
|
}
|
|
854
866
|
this.skipWhitespaceAndComments();
|
|
855
867
|
if (this.token.isOperator()) {
|
|
856
|
-
|
|
868
|
+
if (this.token.isIs()) {
|
|
869
|
+
expression.addNode(this.parseIsOperator());
|
|
870
|
+
} else {
|
|
871
|
+
expression.addNode(this.token.node);
|
|
872
|
+
}
|
|
873
|
+
} else if (this.token.isIn()) {
|
|
874
|
+
expression.addNode(this.parseInOperator());
|
|
875
|
+
} else if (this.token.isContains()) {
|
|
876
|
+
expression.addNode(this.parseContainsOperator());
|
|
877
|
+
} else if (this.token.isStarts()) {
|
|
878
|
+
expression.addNode(this.parseStartsWithOperator());
|
|
879
|
+
} else if (this.token.isEnds()) {
|
|
880
|
+
expression.addNode(this.parseEndsWithOperator());
|
|
881
|
+
} else if (this.token.isNot()) {
|
|
882
|
+
const notOp = this.parseNotOperator();
|
|
883
|
+
if (notOp === null) {
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
expression.addNode(notOp);
|
|
857
887
|
} else {
|
|
858
888
|
break;
|
|
859
889
|
}
|
|
@@ -866,6 +896,86 @@ class Parser extends BaseParser {
|
|
|
866
896
|
return null;
|
|
867
897
|
}
|
|
868
898
|
|
|
899
|
+
private parseIsOperator(): Is | IsNot {
|
|
900
|
+
// Current token is IS. Look ahead for NOT to produce IS NOT.
|
|
901
|
+
const savedIndex = this.tokenIndex;
|
|
902
|
+
this.setNextToken();
|
|
903
|
+
this.skipWhitespaceAndComments();
|
|
904
|
+
if (this.token.isNot()) {
|
|
905
|
+
return new IsNot();
|
|
906
|
+
}
|
|
907
|
+
// Not IS NOT — restore position to IS so the outer loop's setNextToken advances past it.
|
|
908
|
+
this.tokenIndex = savedIndex;
|
|
909
|
+
return new Is();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private parseInOperator(): In | NotIn {
|
|
913
|
+
// Current token is IN. Advance past it so the outer loop's setNextToken moves correctly.
|
|
914
|
+
return new In();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private parseContainsOperator(): Contains {
|
|
918
|
+
return new Contains();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private parseStartsWithOperator(): StartsWith {
|
|
922
|
+
// Current token is STARTS. Look ahead for WITH.
|
|
923
|
+
const savedIndex = this.tokenIndex;
|
|
924
|
+
this.setNextToken();
|
|
925
|
+
this.skipWhitespaceAndComments();
|
|
926
|
+
if (this.token.isWith()) {
|
|
927
|
+
return new StartsWith();
|
|
928
|
+
}
|
|
929
|
+
this.tokenIndex = savedIndex;
|
|
930
|
+
throw new Error("Expected WITH after STARTS");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private parseEndsWithOperator(): EndsWith {
|
|
934
|
+
// Current token is ENDS. Look ahead for WITH.
|
|
935
|
+
const savedIndex = this.tokenIndex;
|
|
936
|
+
this.setNextToken();
|
|
937
|
+
this.skipWhitespaceAndComments();
|
|
938
|
+
if (this.token.isWith()) {
|
|
939
|
+
return new EndsWith();
|
|
940
|
+
}
|
|
941
|
+
this.tokenIndex = savedIndex;
|
|
942
|
+
throw new Error("Expected WITH after ENDS");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
private parseNotOperator(): NotIn | NotContains | NotStartsWith | NotEndsWith | null {
|
|
946
|
+
// Current token is NOT. Look ahead for IN, CONTAINS, STARTS WITH, or ENDS WITH.
|
|
947
|
+
const savedIndex = this.tokenIndex;
|
|
948
|
+
this.setNextToken();
|
|
949
|
+
this.skipWhitespaceAndComments();
|
|
950
|
+
if (this.token.isIn()) {
|
|
951
|
+
return new NotIn();
|
|
952
|
+
}
|
|
953
|
+
if (this.token.isContains()) {
|
|
954
|
+
return new NotContains();
|
|
955
|
+
}
|
|
956
|
+
if (this.token.isStarts()) {
|
|
957
|
+
this.setNextToken();
|
|
958
|
+
this.skipWhitespaceAndComments();
|
|
959
|
+
if (this.token.isWith()) {
|
|
960
|
+
return new NotStartsWith();
|
|
961
|
+
}
|
|
962
|
+
this.tokenIndex = savedIndex;
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
if (this.token.isEnds()) {
|
|
966
|
+
this.setNextToken();
|
|
967
|
+
this.skipWhitespaceAndComments();
|
|
968
|
+
if (this.token.isWith()) {
|
|
969
|
+
return new NotEndsWith();
|
|
970
|
+
}
|
|
971
|
+
this.tokenIndex = savedIndex;
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
// Not a recognized NOT operator — restore position and let the outer loop break.
|
|
975
|
+
this.tokenIndex = savedIndex;
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
|
|
869
979
|
private parseLookup(node: ASTNode): ASTNode {
|
|
870
980
|
let variable: ASTNode = node;
|
|
871
981
|
let lookup: Lookup | RangeLookup | null = null;
|
|
@@ -627,6 +627,30 @@ class Token {
|
|
|
627
627
|
return this._type === TokenType.KEYWORD && this._value === Keyword.IN;
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
public static get CONTAINS(): Token {
|
|
631
|
+
return new Token(TokenType.KEYWORD, Keyword.CONTAINS);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
public isContains(): boolean {
|
|
635
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.CONTAINS;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
public static get STARTS(): Token {
|
|
639
|
+
return new Token(TokenType.KEYWORD, Keyword.STARTS);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
public isStarts(): boolean {
|
|
643
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.STARTS;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
public static get ENDS(): Token {
|
|
647
|
+
return new Token(TokenType.KEYWORD, Keyword.ENDS);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
public isEnds(): boolean {
|
|
651
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.ENDS;
|
|
652
|
+
}
|
|
653
|
+
|
|
630
654
|
public static get PIPE(): Token {
|
|
631
655
|
return new Token(TokenType.KEYWORD, Operator.PIPE);
|
|
632
656
|
}
|