@fincity/kirun-js 2.16.2 → 3.0.0

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.
@@ -0,0 +1,541 @@
1
+ import { ExpressionEvaluationException } from './exception/ExpressionEvaluationException';
2
+ import { ExpressionToken } from './ExpressionToken';
3
+ import { ExpressionTokenValue } from './ExpressionTokenValue';
4
+ import { Expression } from './Expression';
5
+ import { Operation } from './Operation';
6
+ import { ExpressionLexer, Token, TokenType } from './ExpressionLexer';
7
+
8
+ export class ExpressionParser {
9
+ private lexer: ExpressionLexer;
10
+ private currentToken: Token | null = null;
11
+ private previousTokenValue: Token | null = null;
12
+
13
+ constructor(expression: string) {
14
+ this.lexer = new ExpressionLexer(expression);
15
+ this.currentToken = this.lexer.nextToken();
16
+ }
17
+
18
+ public parse(): Expression {
19
+ if (!this.currentToken) {
20
+ throw new ExpressionEvaluationException('', 'Empty expression');
21
+ }
22
+
23
+ const expr = this.parseExpression();
24
+
25
+ // Ensure we consumed all tokens
26
+ if (this.currentToken && this.currentToken.type !== TokenType.EOF) {
27
+ throw new ExpressionEvaluationException(
28
+ this.lexer.getPosition().toString(),
29
+ `Unexpected token: ${this.currentToken.value} at position ${this.currentToken.startPos}`,
30
+ );
31
+ }
32
+
33
+ return expr;
34
+ }
35
+
36
+ private parseExpression(): Expression {
37
+ return this.parseTernary();
38
+ }
39
+
40
+ // Ternary: condition ? trueExpr : falseExpr (precedence 12)
41
+ // Note: Ternary requires 3 tokens, so we use a special constructor pattern
42
+ private parseTernary(): Expression {
43
+ let expr = this.parseLogicalOr();
44
+
45
+ if (this.matchToken(TokenType.QUESTION)) {
46
+ const trueExpr = this.parseTernary();
47
+ this.expectToken(TokenType.COLON);
48
+ const falseExpr = this.parseTernary();
49
+ // Create ternary expression with all 3 tokens: condition, trueExpr, falseExpr
50
+ return Expression.createTernary(expr, trueExpr, falseExpr);
51
+ }
52
+
53
+ return expr;
54
+ }
55
+
56
+ // Logical OR: expr or expr (precedence 11)
57
+ private parseLogicalOr(): Expression {
58
+ let expr = this.parseLogicalAnd();
59
+
60
+ while (this.matchOperator('or')) {
61
+ const right = this.parseLogicalAnd();
62
+ expr = new Expression('', expr, right, Operation.OR);
63
+ }
64
+
65
+ return expr;
66
+ }
67
+
68
+ // Logical AND: expr and expr (precedence 10)
69
+ private parseLogicalAnd(): Expression {
70
+ let expr = this.parseLogicalNot();
71
+
72
+ while (this.matchOperator('and')) {
73
+ const right = this.parseLogicalNot();
74
+ expr = new Expression('', expr, right, Operation.AND);
75
+ }
76
+
77
+ return expr;
78
+ }
79
+
80
+ // Logical NOT: not expr (precedence 10, but unary)
81
+ private parseLogicalNot(): Expression {
82
+ if (this.matchOperator('not')) {
83
+ const expr = this.parseLogicalNot(); // Right-associative for unary
84
+ return new Expression('', expr, undefined, Operation.UNARY_LOGICAL_NOT);
85
+ }
86
+
87
+ return this.parseComparison();
88
+ }
89
+
90
+ // Comparison: <, <=, >, >=, =, != (precedence 5-6)
91
+ private parseComparison(): Expression {
92
+ let expr = this.parseBitwiseOr();
93
+
94
+ while (true) {
95
+ let op: Operation | null = null;
96
+
97
+ if (this.matchOperator('<')) {
98
+ op = Operation.LESS_THAN;
99
+ } else if (this.matchOperator('<=')) {
100
+ op = Operation.LESS_THAN_EQUAL;
101
+ } else if (this.matchOperator('>')) {
102
+ op = Operation.GREATER_THAN;
103
+ } else if (this.matchOperator('>=')) {
104
+ op = Operation.GREATER_THAN_EQUAL;
105
+ } else if (this.matchOperator('=')) {
106
+ op = Operation.EQUAL;
107
+ } else if (this.matchOperator('!=')) {
108
+ op = Operation.NOT_EQUAL;
109
+ } else {
110
+ break;
111
+ }
112
+
113
+ const right = this.parseBitwiseOr();
114
+ expr = new Expression('', expr, right, op);
115
+ }
116
+
117
+ return expr;
118
+ }
119
+
120
+ // Bitwise OR: | (precedence 9)
121
+ private parseBitwiseOr(): Expression {
122
+ let expr = this.parseBitwiseXor();
123
+
124
+ while (this.matchOperator('|')) {
125
+ const right = this.parseBitwiseXor();
126
+ expr = new Expression('', expr, right, Operation.BITWISE_OR);
127
+ }
128
+
129
+ return expr;
130
+ }
131
+
132
+ // Bitwise XOR: ^ (precedence 8)
133
+ private parseBitwiseXor(): Expression {
134
+ let expr = this.parseBitwiseAnd();
135
+
136
+ while (this.matchOperator('^')) {
137
+ const right = this.parseBitwiseAnd();
138
+ expr = new Expression('', expr, right, Operation.BITWISE_XOR);
139
+ }
140
+
141
+ return expr;
142
+ }
143
+
144
+ // Bitwise AND: & (precedence 7)
145
+ private parseBitwiseAnd(): Expression {
146
+ let expr = this.parseShift();
147
+
148
+ while (this.matchOperator('&')) {
149
+ const right = this.parseShift();
150
+ expr = new Expression('', expr, right, Operation.BITWISE_AND);
151
+ }
152
+
153
+ return expr;
154
+ }
155
+
156
+ // Shift: <<, >>, >>> (precedence 4)
157
+ private parseShift(): Expression {
158
+ let expr = this.parseAdditive();
159
+
160
+ while (true) {
161
+ let op: Operation | null = null;
162
+
163
+ if (this.matchOperator('<<')) {
164
+ op = Operation.BITWISE_LEFT_SHIFT;
165
+ } else if (this.matchOperator('>>')) {
166
+ op = Operation.BITWISE_RIGHT_SHIFT;
167
+ } else if (this.matchOperator('>>>')) {
168
+ op = Operation.BITWISE_UNSIGNED_RIGHT_SHIFT;
169
+ } else {
170
+ break;
171
+ }
172
+
173
+ const right = this.parseAdditive();
174
+ expr = new Expression('', expr, right, op);
175
+ }
176
+
177
+ return expr;
178
+ }
179
+
180
+ // Additive: +, - (precedence 3)
181
+ private parseAdditive(): Expression {
182
+ let expr = this.parseMultiplicative();
183
+
184
+ while (true) {
185
+ let op: Operation | null = null;
186
+
187
+ if (this.matchOperator('+')) {
188
+ op = Operation.ADDITION;
189
+ } else if (this.matchOperator('-')) {
190
+ op = Operation.SUBTRACTION;
191
+ } else {
192
+ break;
193
+ }
194
+
195
+ const right = this.parseMultiplicative();
196
+ expr = new Expression('', expr, right, op);
197
+ }
198
+
199
+ return expr;
200
+ }
201
+
202
+ // Multiplicative: *, /, //, % (precedence 2)
203
+ // Note: Right-associative to match old parser behavior (12*13*14/7 = 12*(13*(14/7)))
204
+ private parseMultiplicative(): Expression {
205
+ let expr = this.parseUnary();
206
+
207
+ // Check for multiplicative operators
208
+ if (this.matchOperator('*')) {
209
+ const right = this.parseMultiplicative(); // Right-associative
210
+ return new Expression('', expr, right, Operation.MULTIPLICATION);
211
+ } else if (this.matchOperator('/')) {
212
+ const right = this.parseMultiplicative(); // Right-associative
213
+ return new Expression('', expr, right, Operation.DIVISION);
214
+ } else if (this.matchOperator('//')) {
215
+ const right = this.parseMultiplicative(); // Right-associative
216
+ return new Expression('', expr, right, Operation.INTEGER_DIVISION);
217
+ } else if (this.matchOperator('%')) {
218
+ const right = this.parseMultiplicative(); // Right-associative
219
+ return new Expression('', expr, right, Operation.MOD);
220
+ }
221
+
222
+ return expr;
223
+ }
224
+
225
+ // Unary: +, -, ~, not (precedence 1)
226
+ private parseUnary(): Expression {
227
+ if (this.matchOperator('+')) {
228
+ const expr = this.parseUnary();
229
+ return new Expression('', expr, undefined, Operation.UNARY_PLUS);
230
+ }
231
+
232
+ if (this.matchOperator('-')) {
233
+ const expr = this.parseUnary();
234
+ return new Expression('', expr, undefined, Operation.UNARY_MINUS);
235
+ }
236
+
237
+ if (this.matchOperator('~')) {
238
+ const expr = this.parseUnary();
239
+ return new Expression('', expr, undefined, Operation.UNARY_BITWISE_COMPLEMENT);
240
+ }
241
+
242
+ return this.parsePostfix();
243
+ }
244
+
245
+ // Postfix: member access, array access (precedence 1)
246
+ // Note: When we have Context.a[...], we need to parse it as Context.(a[...])
247
+ // not (Context.a)[...]. This means array access on an identifier should be
248
+ // grouped with that identifier before applying the object operator.
249
+ private parsePostfix(): Expression {
250
+ let expr = this.parsePrimary();
251
+
252
+ while (true) {
253
+ // Object member access: .identifier
254
+ // This must parse the entire right-hand side (including array access) before applying
255
+ if (this.matchToken(TokenType.DOT)) {
256
+ // Parse identifier and all its postfix operations (array access, more dots, etc.)
257
+ // This will consume all tokens up to the next non-postfix operator
258
+ const right = this.parsePostfixRightSide();
259
+ expr = new Expression('', expr, right, Operation.OBJECT_OPERATOR);
260
+ // Don't continue - parsePostfixRightSide() should have consumed everything
261
+ // If there are more postfix operations, they should be part of the right-hand side
262
+ continue;
263
+ }
264
+ // Array access: [expression] - for array access directly on the current expression
265
+ // Only if we didn't just process a dot (dot handling is above)
266
+ else if (this.matchToken(TokenType.LEFT_BRACKET)) {
267
+ const indexExpr = this.parseBracketContent();
268
+ this.expectToken(TokenType.RIGHT_BRACKET);
269
+ expr = new Expression('', expr, indexExpr, Operation.ARRAY_OPERATOR);
270
+ }
271
+ // Range operator: ..
272
+ else if (this.matchOperator('..')) {
273
+ const right = this.parsePrimary();
274
+ expr = new Expression('', expr, right, Operation.ARRAY_RANGE_INDEX_OPERATOR);
275
+ } else {
276
+ break;
277
+ }
278
+ }
279
+
280
+ return expr;
281
+ }
282
+
283
+ // Parse the right side of a dot operator (identifier that may have static array access)
284
+ // The lexer now only includes STATIC bracket content in identifiers (numeric or quoted strings)
285
+ // Dynamic bracket content ([Page.id], [expr]) is tokenized separately as LEFT_BRACKET
286
+ //
287
+ // This method consumes ALL subsequent property accesses (dots) and array accesses,
288
+ // creating a grouped right-hand side for the OBJECT_OPERATOR.
289
+ //
290
+ // Examples:
291
+ // - "Context.obj[\"key\"]" -> Context . (obj["key"])
292
+ // - "Context.a.b.c" -> Context . (a . (b . c))
293
+ // - "Context.obj[\"key\"].value" -> Context . (obj["key"] . value)
294
+ private parsePostfixRightSide(): Expression {
295
+ // Expect an identifier (which may include STATIC bracket notation like obj["key"] or a[9])
296
+ if (!this.currentToken || this.currentToken.type !== TokenType.IDENTIFIER) {
297
+ throw new ExpressionEvaluationException(
298
+ this.lexer.getPosition().toString(),
299
+ 'Expected identifier after dot',
300
+ );
301
+ }
302
+
303
+ // The identifier token value might contain static bracket notation like "obj[\"key\"]" or "a[9]"
304
+ // Or it might be a plain identifier if bracket content is dynamic
305
+ const identifierValue = this.currentToken.value;
306
+ this.advance();
307
+
308
+ // Check if the identifier contains static bracket notation
309
+ const bracketIndex = identifierValue.indexOf('[');
310
+ let expr: Expression;
311
+
312
+ if (bracketIndex === -1) {
313
+ // No bracket notation in the identifier - it's a simple identifier
314
+ // Use createLeaf to avoid re-parsing
315
+ expr = Expression.createLeaf(identifierValue);
316
+ } else {
317
+ // Static bracket notation is included in the identifier
318
+ // Parse it to extract base identifier and static bracket expressions
319
+ expr = this.parseStaticBracketIdentifier(identifierValue);
320
+ }
321
+
322
+ // Check for dynamic array access following the identifier (LEFT_BRACKET token)
323
+ while (this.matchToken(TokenType.LEFT_BRACKET)) {
324
+ const indexExpr = this.parseBracketContent();
325
+ this.expectToken(TokenType.RIGHT_BRACKET);
326
+ expr = new Expression('', expr, indexExpr, Operation.ARRAY_OPERATOR);
327
+ }
328
+
329
+ // Also consume any subsequent DOTs to group all property accesses on the right side
330
+ // This creates the structure: Context . (a . (b . c)) instead of ((Context . a) . b) . c
331
+ while (this.matchToken(TokenType.DOT)) {
332
+ const right = this.parsePostfixRightSide(); // Recursive call
333
+ expr = new Expression('', expr, right, Operation.OBJECT_OPERATOR);
334
+ }
335
+
336
+ return expr;
337
+ }
338
+
339
+ /**
340
+ * Parse an identifier that contains static bracket notation.
341
+ * E.g., "obj[\"key\"]" or "a[9]" or "a[9][\"key\"]"
342
+ */
343
+ private parseStaticBracketIdentifier(identifierValue: string): Expression {
344
+ const bracketIndex = identifierValue.indexOf('[');
345
+
346
+ // Extract base identifier - use createLeaf to avoid re-parsing
347
+ const baseIdentifier = identifierValue.substring(0, bracketIndex);
348
+ let expr = Expression.createLeaf(baseIdentifier);
349
+
350
+ // Parse all static bracket expressions
351
+ let remaining = identifierValue.substring(bracketIndex);
352
+ let bracketStart = 0;
353
+
354
+ while (bracketStart < remaining.length && remaining[bracketStart] === '[') {
355
+ // Find the matching closing bracket
356
+ let bracketCount = 1;
357
+ let endIndex = bracketStart + 1;
358
+ let inString = false;
359
+ let stringChar = '';
360
+
361
+ while (endIndex < remaining.length && bracketCount > 0) {
362
+ const c = remaining[endIndex];
363
+
364
+ if (inString) {
365
+ if (c === stringChar && (endIndex === 0 || remaining[endIndex - 1] !== '\\')) {
366
+ inString = false;
367
+ }
368
+ } else {
369
+ if (c === '"' || c === "'") {
370
+ inString = true;
371
+ stringChar = c;
372
+ } else if (c === '[') {
373
+ bracketCount++;
374
+ } else if (c === ']') {
375
+ bracketCount--;
376
+ }
377
+ }
378
+ endIndex++;
379
+ }
380
+
381
+ // Extract bracket content (without the brackets)
382
+ const bracketContent = remaining.substring(bracketStart + 1, endIndex - 1);
383
+
384
+ // Create expression for this bracket content
385
+ let indexExpr: Expression;
386
+ if ((bracketContent.startsWith('"') && bracketContent.endsWith('"')) ||
387
+ (bracketContent.startsWith("'") && bracketContent.endsWith("'"))) {
388
+ // It's a string literal - preserve quotes
389
+ const quoteChar = bracketContent[0];
390
+ const strValue = bracketContent.substring(1, bracketContent.length - 1);
391
+ indexExpr = new Expression('', new ExpressionTokenValue(quoteChar + strValue + quoteChar, strValue), undefined, undefined);
392
+ } else {
393
+ // It's a number or range (static content from lexer)
394
+ // Check for range operator
395
+ const rangeIndex = bracketContent.indexOf('..');
396
+ if (rangeIndex !== -1) {
397
+ // Range expression like "0..5"
398
+ const startExpr = rangeIndex === 0 ? Expression.createLeaf('0') : Expression.createLeaf(bracketContent.substring(0, rangeIndex));
399
+ const endExpr = rangeIndex === bracketContent.length - 2 ? Expression.createLeaf('') : Expression.createLeaf(bracketContent.substring(rangeIndex + 2));
400
+ indexExpr = new Expression('', startExpr, endExpr, Operation.ARRAY_RANGE_INDEX_OPERATOR);
401
+ } else {
402
+ // Simple number - use createLeaf to avoid re-parsing
403
+ indexExpr = Expression.createLeaf(bracketContent);
404
+ }
405
+ }
406
+
407
+ // Create array access expression
408
+ expr = new Expression('', expr, indexExpr, Operation.ARRAY_OPERATOR);
409
+
410
+ bracketStart = endIndex;
411
+ }
412
+
413
+ return expr;
414
+ }
415
+
416
+ // Parse identifier path - the lexer now handles paths with STATIC bracket notation only
417
+ // Dynamic bracket content is tokenized separately as LEFT_BRACKET
418
+ //
419
+ // Examples:
420
+ // - "Context.obj[\"key\"]" (static) -> single IDENTIFIER token
421
+ // - "Context.a[9]" (static) -> single IDENTIFIER token
422
+ // - "Context.a[Page.id]" (dynamic) -> IDENTIFIER "Context.a" + LEFT_BRACKET + expression
423
+ private parseIdentifierPath(): Expression {
424
+ if (!this.currentToken || this.currentToken.type !== TokenType.IDENTIFIER) {
425
+ throw new ExpressionEvaluationException(
426
+ this.lexer.getPosition().toString(),
427
+ 'Expected identifier',
428
+ );
429
+ }
430
+
431
+ // The identifier token contains the path with static brackets (e.g., "Context.obj[\"key\"]")
432
+ // or a plain path if bracket content is dynamic (e.g., "Context.a")
433
+ const path = this.currentToken.value;
434
+ this.advance();
435
+
436
+ // For paths with static bracket notation, return as single identifier for TokenValueExtractor
437
+ // The evaluator will use TokenValueExtractor to resolve the entire path efficiently
438
+ // Use createLeaf to avoid re-parsing
439
+ return Expression.createLeaf(path);
440
+ }
441
+
442
+ // Parse bracket content - can be expression, string literal, or identifier
443
+ private parseBracketContent(): Expression {
444
+ // If it's a string literal (quoted), preserve it
445
+ if (this.currentToken && this.currentToken.type === TokenType.STRING) {
446
+ const token = this.currentToken;
447
+ this.advance();
448
+ const strValue = token.value.substring(1, token.value.length - 1);
449
+ return new Expression('', new ExpressionTokenValue(token.value, strValue), undefined, undefined);
450
+ }
451
+
452
+ // Otherwise parse as expression
453
+ return this.parseExpression();
454
+ }
455
+
456
+ // Primary: literals, identifiers, parentheses, groups
457
+ private parsePrimary(): Expression {
458
+ // Number literal - wrap in Expression with ExpressionToken as token
459
+ if (this.matchToken(TokenType.NUMBER)) {
460
+ const token = this.previousToken()!;
461
+ // Create an Expression containing a single ExpressionToken (no operations)
462
+ // This matches the old parser's structure
463
+ return new Expression('', new ExpressionToken(token.value), undefined, undefined);
464
+ }
465
+
466
+ // String literal
467
+ if (this.matchToken(TokenType.STRING)) {
468
+ const token = this.previousToken()!;
469
+ // Remove quotes for the value, but keep original expression for bracket notation
470
+ const strValue = token.value.substring(1, token.value.length - 1);
471
+ // Create ExpressionTokenValue with original quoted string as expression
472
+ return new Expression('', new ExpressionTokenValue(token.value, strValue), undefined, undefined);
473
+ }
474
+
475
+ // Identifier (may contain dots for paths like "Context.obj")
476
+ if (this.currentToken && this.currentToken.type === TokenType.IDENTIFIER) {
477
+ return this.parseIdentifierPath();
478
+ }
479
+
480
+ // Parenthesized expression
481
+ if (this.matchToken(TokenType.LEFT_PAREN)) {
482
+ const expr = this.parseExpression();
483
+ this.expectToken(TokenType.RIGHT_PAREN);
484
+ return expr;
485
+ }
486
+
487
+ // Nullish coalescing: ??
488
+ if (this.matchOperator('??')) {
489
+ const right = this.parsePrimary();
490
+ // This should be handled at a higher level, but for now we'll treat it here
491
+ return new Expression('', this.parsePrimary(), right, Operation.NULLISH_COALESCING_OPERATOR);
492
+ }
493
+
494
+ throw new ExpressionEvaluationException(
495
+ this.lexer.getPosition().toString(),
496
+ `Unexpected token: ${this.currentToken?.value || 'EOF'} at position ${this.currentToken?.startPos || this.lexer.getPosition()}`,
497
+ );
498
+ }
499
+
500
+ // Helper methods
501
+ private matchToken(type: TokenType): boolean {
502
+ if (this.currentToken && this.currentToken.type === type) {
503
+ this.advance();
504
+ return true;
505
+ }
506
+ return false;
507
+ }
508
+
509
+ private matchOperator(op: string): boolean {
510
+ if (
511
+ this.currentToken &&
512
+ this.currentToken.type === TokenType.OPERATOR &&
513
+ this.currentToken.value === op
514
+ ) {
515
+ this.advance();
516
+ return true;
517
+ }
518
+ return false;
519
+ }
520
+
521
+ private expectToken(type: TokenType): Token {
522
+ if (!this.currentToken || this.currentToken.type !== type) {
523
+ throw new ExpressionEvaluationException(
524
+ this.lexer.getPosition().toString(),
525
+ `Expected ${type}, got ${this.currentToken?.type || 'EOF'}`,
526
+ );
527
+ }
528
+ const token = this.currentToken;
529
+ this.advance();
530
+ return token;
531
+ }
532
+
533
+ private advance(): void {
534
+ this.previousTokenValue = this.currentToken;
535
+ this.currentToken = this.lexer.nextToken();
536
+ }
537
+
538
+ private previousToken(): Token | null {
539
+ return this.previousTokenValue;
540
+ }
541
+ }
@@ -0,0 +1,21 @@
1
+ // Debug helper to trace parser behavior
2
+ import { ExpressionParser } from './ExpressionParser';
3
+ import { Expression } from './Expression';
4
+
5
+ export function debugParse(expr: string): void {
6
+ console.log(`\n=== Parsing: ${expr} ===`);
7
+ try {
8
+ const parser = new ExpressionParser(expr);
9
+ const result = parser.parse();
10
+ console.log('Tokens:', result.getTokens().toArray().map(t => t.toString()));
11
+ console.log('Ops:', result.getOperations().toArray().map(o => o.getOperator()));
12
+ console.log('ToString:', result.toString());
13
+ } catch (error: any) {
14
+ console.error('Error:', error.message);
15
+ }
16
+ }
17
+
18
+ // Test cases
19
+ debugParse('Context.a[1]');
20
+ debugParse('Context.a');
21
+ debugParse('a[1]');
@@ -144,6 +144,7 @@ export class Operation {
144
144
  [Operation.BITWISE_AND, 7],
145
145
  [Operation.BITWISE_XOR, 8],
146
146
  [Operation.BITWISE_OR, 9],
147
+ [Operation.NOT, 10], // NOT has same precedence as AND, lower than comparisons
147
148
  [Operation.AND, 10],
148
149
  [Operation.OR, 11],
149
150
  [Operation.NULLISH_COALESCING_OPERATOR, 11],