@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.
- package/__tests__/engine/runtime/expression/ExpressionEvaluatorLengthSubtractionBugTest.ts +384 -0
- package/__tests__/engine/runtime/expression/ExpressionEvaluatorLengthSubtractionTest.ts +338 -0
- package/__tests__/engine/runtime/expression/ExpressionTest.ts +7 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +56 -39
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/engine/function/system/context/SetFunction.ts +226 -75
- package/src/engine/runtime/expression/Expression.ts +267 -71
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +317 -22
- package/src/engine/runtime/expression/ExpressionLexer.ts +365 -0
- package/src/engine/runtime/expression/ExpressionParser.ts +541 -0
- package/src/engine/runtime/expression/ExpressionParserDebug.ts +21 -0
- package/src/engine/runtime/expression/Operation.ts +1 -0
- package/src/engine/runtime/expression/tokenextractor/ObjectValueSetterExtractor.ts +134 -31
- package/src/engine/runtime/expression/tokenextractor/TokenValueExtractor.ts +32 -8
- package/src/engine/util/LinkedList.ts +1 -1
|
@@ -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],
|