@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
|
@@ -2,8 +2,6 @@ import { KIRuntimeException } from '../../../exception/KIRuntimeException';
|
|
|
2
2
|
import { isNullValue } from '../../../util/NullCheck';
|
|
3
3
|
import { duplicate } from '../../../util/duplicate';
|
|
4
4
|
import { StringFormatter } from '../../../util/string/StringFormatter';
|
|
5
|
-
import { Expression } from '../Expression';
|
|
6
|
-
import { ExpressionTokenValue } from '../ExpressionTokenValue';
|
|
7
5
|
import { Operation } from '../Operation';
|
|
8
6
|
import { TokenValueExtractor } from './TokenValueExtractor';
|
|
9
7
|
|
|
@@ -16,7 +14,7 @@ export class ObjectValueSetterExtractor extends TokenValueExtractor {
|
|
|
16
14
|
this.prefix = prefix;
|
|
17
15
|
}
|
|
18
16
|
protected getValueInternal(token: string) {
|
|
19
|
-
let parts: string[] =
|
|
17
|
+
let parts: string[] = TokenValueExtractor.splitPath(token);
|
|
20
18
|
return this.retrieveElementFrom(token, parts, 1, this.store);
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -44,38 +42,143 @@ export class ObjectValueSetterExtractor extends TokenValueExtractor {
|
|
|
44
42
|
overwrite: boolean,
|
|
45
43
|
deleteOnNull: boolean,
|
|
46
44
|
) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
// Use TokenValueExtractor.splitPath to get path segments instead of Expression parsing
|
|
46
|
+
// This is more reliable as it directly handles the path string
|
|
47
|
+
const parts = TokenValueExtractor.splitPath(stringToken);
|
|
48
|
+
|
|
49
|
+
if (parts.length < 2) {
|
|
50
|
+
throw new KIRuntimeException(
|
|
51
|
+
StringFormatter.format('Invalid path: $', stringToken),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Start from index 1 (skip the prefix like 'Store')
|
|
59
56
|
let el = this.store;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
|
|
58
|
+
// Navigate to the parent of the final element
|
|
59
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
60
|
+
const part = parts[i];
|
|
61
|
+
const nextPart = parts[i + 1];
|
|
62
|
+
|
|
63
|
+
// Parse bracket segments within this part
|
|
64
|
+
const segments = this.parseBracketSegments(part);
|
|
65
|
+
|
|
66
|
+
for (let j = 0; j < segments.length; j++) {
|
|
67
|
+
const segment = segments[j];
|
|
68
|
+
const isLastSegment = (i === parts.length - 2 && j === segments.length - 1);
|
|
69
|
+
const nextOp = isLastSegment ? this.getOpForSegment(parts[parts.length - 1]) : this.getOpForSegment(nextPart);
|
|
70
|
+
|
|
71
|
+
if (this.isArrayIndex(segment)) {
|
|
72
|
+
el = this.getDataFromArray(el, segment, nextOp);
|
|
73
|
+
} else {
|
|
74
|
+
el = this.getDataFromObject(el, this.stripQuotes(segment), nextOp);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle the final part (set the value)
|
|
80
|
+
const finalPart = parts[parts.length - 1];
|
|
81
|
+
const finalSegments = this.parseBracketSegments(finalPart);
|
|
82
|
+
|
|
83
|
+
// Navigate through all but the last segment of the final part
|
|
84
|
+
for (let j = 0; j < finalSegments.length - 1; j++) {
|
|
85
|
+
const segment = finalSegments[j];
|
|
86
|
+
const nextOp = this.isArrayIndex(finalSegments[j + 1]) ? Operation.ARRAY_OPERATOR : Operation.OBJECT_OPERATOR;
|
|
87
|
+
|
|
88
|
+
if (this.isArrayIndex(segment)) {
|
|
89
|
+
el = this.getDataFromArray(el, segment, nextOp);
|
|
64
90
|
} else {
|
|
65
|
-
el = this.
|
|
91
|
+
el = this.getDataFromObject(el, this.stripQuotes(segment), nextOp);
|
|
66
92
|
}
|
|
67
|
-
|
|
68
|
-
op = ops.removeLast();
|
|
69
|
-
token = tokens.removeLast();
|
|
70
|
-
mem =
|
|
71
|
-
token instanceof ExpressionTokenValue
|
|
72
|
-
? (token as ExpressionTokenValue).getElement()
|
|
73
|
-
: token.getExpression();
|
|
74
93
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
|
|
95
|
+
// Set the final value
|
|
96
|
+
const lastSegment = finalSegments[finalSegments.length - 1];
|
|
97
|
+
if (this.isArrayIndex(lastSegment)) {
|
|
98
|
+
this.putDataInArray(el, lastSegment, value, overwrite, deleteOnNull);
|
|
99
|
+
} else {
|
|
100
|
+
this.putDataInObject(el, this.stripQuotes(lastSegment), value, overwrite, deleteOnNull);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse a path segment that may contain bracket notation.
|
|
106
|
+
* E.g., "addresses[0]" -> ["addresses", "0"]
|
|
107
|
+
* E.g., 'obj["key"]' -> ["obj", "key"]
|
|
108
|
+
*/
|
|
109
|
+
private parseBracketSegments(part: string): string[] {
|
|
110
|
+
const segments: string[] = [];
|
|
111
|
+
let start = 0;
|
|
112
|
+
let i = 0;
|
|
113
|
+
|
|
114
|
+
while (i < part.length) {
|
|
115
|
+
if (part[i] === '[') {
|
|
116
|
+
if (i > start) {
|
|
117
|
+
segments.push(part.substring(start, i));
|
|
118
|
+
}
|
|
119
|
+
// Find matching ]
|
|
120
|
+
let end = i + 1;
|
|
121
|
+
let inQuote = false;
|
|
122
|
+
let quoteChar = '';
|
|
123
|
+
while (end < part.length) {
|
|
124
|
+
if (inQuote) {
|
|
125
|
+
if (part[end] === quoteChar && part[end - 1] !== '\\') {
|
|
126
|
+
inQuote = false;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
if (part[end] === '"' || part[end] === "'") {
|
|
130
|
+
inQuote = true;
|
|
131
|
+
quoteChar = part[end];
|
|
132
|
+
} else if (part[end] === ']') {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
end++;
|
|
137
|
+
}
|
|
138
|
+
// Extract bracket content (without the brackets)
|
|
139
|
+
segments.push(part.substring(i + 1, end));
|
|
140
|
+
start = end + 1;
|
|
141
|
+
i = start;
|
|
142
|
+
} else {
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (start < part.length) {
|
|
148
|
+
segments.push(part.substring(start));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return segments.length > 0 ? segments : [part];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a segment is an array index (numeric)
|
|
156
|
+
*/
|
|
157
|
+
private isArrayIndex(segment: string): boolean {
|
|
158
|
+
// Check if it's a pure number (possibly negative)
|
|
159
|
+
return /^-?\d+$/.test(segment);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Strip quotes from a segment if present
|
|
164
|
+
*/
|
|
165
|
+
private stripQuotes(segment: string): string {
|
|
166
|
+
if ((segment.startsWith('"') && segment.endsWith('"')) ||
|
|
167
|
+
(segment.startsWith("'") && segment.endsWith("'"))) {
|
|
168
|
+
return segment.substring(1, segment.length - 1);
|
|
169
|
+
}
|
|
170
|
+
return segment;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Determine the operation type for the next segment
|
|
175
|
+
*/
|
|
176
|
+
private getOpForSegment(segment: string): Operation {
|
|
177
|
+
// Check if the segment starts with a bracket or is a pure number
|
|
178
|
+
if (this.isArrayIndex(segment) || segment.startsWith('[')) {
|
|
179
|
+
return Operation.ARRAY_OPERATOR;
|
|
180
|
+
}
|
|
181
|
+
return Operation.OBJECT_OPERATOR;
|
|
79
182
|
}
|
|
80
183
|
|
|
81
184
|
private getDataFromArray(el: any, mem: string, nextOp: Operation): any {
|
|
@@ -143,13 +143,23 @@ export abstract class TokenValueExtractor {
|
|
|
143
143
|
|
|
144
144
|
// Fast path: simple property access on object (most common case)
|
|
145
145
|
if (typeof element === 'object' && !Array.isArray(element)) {
|
|
146
|
-
if
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Check for 'length' on object
|
|
146
|
+
// For 'length' on objects, check if there's a length property
|
|
147
|
+
// If it's a primitive (number, string, boolean), use it
|
|
148
|
+
// If it's an object/array, use Object.keys length to avoid bugs
|
|
150
149
|
if (segment === 'length') {
|
|
150
|
+
if ('length' in element) {
|
|
151
|
+
const lengthValue = element['length'];
|
|
152
|
+
// If length property is a primitive, use it; otherwise use Object.keys length
|
|
153
|
+
if (typeof lengthValue === 'object' && lengthValue !== null) {
|
|
154
|
+
return Object.keys(element).length;
|
|
155
|
+
}
|
|
156
|
+
return lengthValue;
|
|
157
|
+
}
|
|
151
158
|
return Object.keys(element).length;
|
|
152
159
|
}
|
|
160
|
+
if (segment in element) {
|
|
161
|
+
return element[segment];
|
|
162
|
+
}
|
|
153
163
|
return element[segment];
|
|
154
164
|
}
|
|
155
165
|
|
|
@@ -191,7 +201,10 @@ export abstract class TokenValueExtractor {
|
|
|
191
201
|
): any {
|
|
192
202
|
if (isNullValue(cElement)) return undefined;
|
|
193
203
|
|
|
194
|
-
|
|
204
|
+
// Check for 'length' keyword - both unquoted and quoted versions
|
|
205
|
+
// e.g., .length and ["length"] should both return the length
|
|
206
|
+
if (cPart === 'length' || cPart === '"length"' || cPart === "'length'")
|
|
207
|
+
return this.getLength(token, cElement);
|
|
195
208
|
|
|
196
209
|
if (typeof cElement == 'string' || Array.isArray(cElement))
|
|
197
210
|
return this.handleArrayAccess(token, cPart, cElement);
|
|
@@ -204,8 +217,18 @@ export abstract class TokenValueExtractor {
|
|
|
204
217
|
|
|
205
218
|
if (type === 'string' || Array.isArray(cElement)) return cElement.length;
|
|
206
219
|
if (type === 'object') {
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
// For objects, check if there's a length property
|
|
221
|
+
// If it's a primitive (number, string, boolean), use it
|
|
222
|
+
// If it's an object/array, use Object.keys length to avoid bugs
|
|
223
|
+
if ('length' in cElement) {
|
|
224
|
+
const lengthValue = cElement['length'];
|
|
225
|
+
// If length property is a primitive, use it; otherwise use Object.keys length
|
|
226
|
+
if (typeof lengthValue === 'object' && lengthValue !== null) {
|
|
227
|
+
return Object.keys(cElement).length;
|
|
228
|
+
}
|
|
229
|
+
return lengthValue;
|
|
230
|
+
}
|
|
231
|
+
return Object.keys(cElement).length;
|
|
209
232
|
}
|
|
210
233
|
|
|
211
234
|
throw new ExpressionEvaluationException(
|
|
@@ -263,7 +286,8 @@ export abstract class TokenValueExtractor {
|
|
|
263
286
|
// Handle both single and double quoted keys
|
|
264
287
|
if (cPart.startsWith('"') || cPart.startsWith("'")) {
|
|
265
288
|
const quoteChar = cPart[0];
|
|
266
|
-
|
|
289
|
+
// Allow empty string key: "" or ''
|
|
290
|
+
if (!cPart.endsWith(quoteChar) || cPart.length == 1) {
|
|
267
291
|
throw new ExpressionEvaluationException(
|
|
268
292
|
token,
|
|
269
293
|
StringFormatter.format('$ is missing a closing quote or empty key found', token),
|