@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
|
@@ -471,6 +471,23 @@ export class ExpressionEvaluator {
|
|
|
471
471
|
if (workingStack.length > 0) {
|
|
472
472
|
return workingStack.pop()!;
|
|
473
473
|
}
|
|
474
|
+
if (ctx.srcIdx >= tokensSource.length) {
|
|
475
|
+
// Only throw if we're actively processing operations and need a token
|
|
476
|
+
// This indicates a malformed expression (not enough tokens for operations)
|
|
477
|
+
// Check if we have more operations to process
|
|
478
|
+
if (ctx.opIdx < opsArray.length) {
|
|
479
|
+
throw new ExpressionEvaluationException(
|
|
480
|
+
exp.getExpression(),
|
|
481
|
+
'Not enough tokens to evaluate expression',
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
// If we're done with operations but need a token, this is an error
|
|
485
|
+
// This can happen with malformed expressions
|
|
486
|
+
throw new ExpressionEvaluationException(
|
|
487
|
+
exp.getExpression(),
|
|
488
|
+
'Expression evaluation incomplete: missing token',
|
|
489
|
+
);
|
|
490
|
+
}
|
|
474
491
|
return tokensSource[ctx.srcIdx++];
|
|
475
492
|
};
|
|
476
493
|
|
|
@@ -562,27 +579,51 @@ export class ExpressionEvaluator {
|
|
|
562
579
|
|
|
563
580
|
do {
|
|
564
581
|
objOperations.push(operator);
|
|
565
|
-
if (token instanceof Expression)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
582
|
+
if (token instanceof Expression) {
|
|
583
|
+
// For path components (identifiers with OBJECT_OPERATOR, ARRAY_OPERATOR, or no operations),
|
|
584
|
+
// build the path string without parentheses - don't evaluate as a value.
|
|
585
|
+
// For expressions with other operators (like +, -, etc.), evaluate to get the actual value.
|
|
586
|
+
if (this.isPathExpression(token)) {
|
|
587
|
+
// Build path string without parentheses
|
|
588
|
+
const tokenStr = this.buildPathString(token);
|
|
589
|
+
objTokens.push(new ExpressionToken(tokenStr));
|
|
590
|
+
} else {
|
|
591
|
+
objTokens.push(
|
|
592
|
+
new ExpressionTokenValue(
|
|
593
|
+
token.toString(),
|
|
594
|
+
this.evaluateExpression(token, valuesMap),
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
572
599
|
else if (token) objTokens.push(token);
|
|
573
600
|
|
|
574
|
-
|
|
601
|
+
// Match Java logic: check workingStack first, then tokensSource
|
|
602
|
+
if (workingStack.length > 0) {
|
|
603
|
+
token = workingStack.pop();
|
|
604
|
+
} else if (ctx.srcIdx < tokensSource.length) {
|
|
605
|
+
token = tokensSource[ctx.srcIdx++];
|
|
606
|
+
} else {
|
|
607
|
+
token = undefined;
|
|
608
|
+
}
|
|
575
609
|
operator = popOp();
|
|
576
610
|
} while (operator == Operation.OBJECT_OPERATOR || operator == Operation.ARRAY_OPERATOR);
|
|
577
611
|
|
|
578
612
|
if (token) {
|
|
579
|
-
if (token instanceof Expression)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
613
|
+
if (token instanceof Expression) {
|
|
614
|
+
// Same logic: path components use buildPathString(), value expressions are evaluated
|
|
615
|
+
if (this.isPathExpression(token)) {
|
|
616
|
+
const tokenStr = this.buildPathString(token);
|
|
617
|
+
objTokens.push(new ExpressionToken(tokenStr));
|
|
618
|
+
} else {
|
|
619
|
+
objTokens.push(
|
|
620
|
+
new ExpressionTokenValue(
|
|
621
|
+
token.toString(),
|
|
622
|
+
this.evaluateExpression(token, valuesMap),
|
|
623
|
+
),
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
586
627
|
else objTokens.push(token);
|
|
587
628
|
}
|
|
588
629
|
|
|
@@ -607,16 +648,41 @@ export class ExpressionEvaluator {
|
|
|
607
648
|
}
|
|
608
649
|
|
|
609
650
|
// Use string concatenation instead of StringBuilder (V8 optimizes this well)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
651
|
+
// Preserve quotes for bracket notation with quoted keys containing dots (like ["mail.props.port"])
|
|
652
|
+
let str: string;
|
|
653
|
+
if (objToken instanceof ExpressionTokenValue) {
|
|
654
|
+
const originalExpr = objToken.getExpression();
|
|
655
|
+
const evaluatedValue = objToken.getTokenValue();
|
|
656
|
+
// Preserve quotes when the key contains dots to distinguish from simple bracket access
|
|
657
|
+
if (originalExpr && originalExpr.length > 0 &&
|
|
658
|
+
(originalExpr.charAt(0) == '"' || originalExpr.charAt(0) == "'") &&
|
|
659
|
+
typeof evaluatedValue === 'string' && evaluatedValue.includes('.')) {
|
|
660
|
+
str = originalExpr;
|
|
661
|
+
} else {
|
|
662
|
+
str = typeof evaluatedValue === 'string' ? evaluatedValue : String(evaluatedValue);
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
str = objToken.toString();
|
|
666
|
+
}
|
|
613
667
|
|
|
614
668
|
while (objTokenIdx >= 0) {
|
|
615
669
|
objToken = objTokens[objTokenIdx--];
|
|
616
670
|
operator = objOperations[objOpIdx--];
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
671
|
+
let tokenVal: string;
|
|
672
|
+
if (objToken instanceof ExpressionTokenValue) {
|
|
673
|
+
const originalExpr = objToken.getExpression();
|
|
674
|
+
const evaluatedValue = objToken.getTokenValue();
|
|
675
|
+
// Preserve quotes when the key contains dots
|
|
676
|
+
if (operator == Operation.ARRAY_OPERATOR && originalExpr && originalExpr.length > 0 &&
|
|
677
|
+
(originalExpr.charAt(0) == '"' || originalExpr.charAt(0) == "'") &&
|
|
678
|
+
typeof evaluatedValue === 'string' && evaluatedValue.includes('.')) {
|
|
679
|
+
tokenVal = originalExpr;
|
|
680
|
+
} else {
|
|
681
|
+
tokenVal = typeof evaluatedValue === 'string' ? evaluatedValue : String(evaluatedValue);
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
tokenVal = objToken.toString();
|
|
685
|
+
}
|
|
620
686
|
str = str + operator!.getOperator() + tokenVal + (operator == Operation.ARRAY_OPERATOR ? ']' : '');
|
|
621
687
|
}
|
|
622
688
|
let key: string = str.substring(0, str.indexOf('.') + 1);
|
|
@@ -627,12 +693,127 @@ export class ExpressionEvaluator {
|
|
|
627
693
|
try {
|
|
628
694
|
v = LiteralTokenValueExtractor.INSTANCE.getValue(str);
|
|
629
695
|
} catch (err) {
|
|
630
|
-
|
|
696
|
+
// Check if this is a literal (number, string, boolean, null) with property access
|
|
697
|
+
// e.g., "2.val" should evaluate to undefined (accessing .val on number 2)
|
|
698
|
+
v = this.evaluateLiteralPropertyAccess(str);
|
|
631
699
|
}
|
|
632
700
|
workingStack.push(new ExpressionTokenValue(str, v));
|
|
633
701
|
}
|
|
634
702
|
}
|
|
635
703
|
|
|
704
|
+
/**
|
|
705
|
+
* Handle cases like "2.val" where we have a literal with property access.
|
|
706
|
+
* Numbers, booleans, null don't have custom properties, so accessing them returns undefined.
|
|
707
|
+
* Strings might have properties like .length
|
|
708
|
+
*/
|
|
709
|
+
private evaluateLiteralPropertyAccess(str: string): any {
|
|
710
|
+
const dotIdx = str.indexOf('.');
|
|
711
|
+
if (dotIdx === -1) {
|
|
712
|
+
// No property access, just return the string as-is
|
|
713
|
+
return str;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const basePart = str.substring(0, dotIdx);
|
|
717
|
+
const propPart = str.substring(dotIdx + 1);
|
|
718
|
+
|
|
719
|
+
// Try to parse the base as a literal
|
|
720
|
+
let baseValue: any;
|
|
721
|
+
try {
|
|
722
|
+
baseValue = LiteralTokenValueExtractor.INSTANCE.getValue(basePart);
|
|
723
|
+
} catch (err) {
|
|
724
|
+
// Not a valid literal, return the original string
|
|
725
|
+
return str;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// If baseValue is null or undefined, property access returns undefined
|
|
729
|
+
if (baseValue === null || baseValue === undefined) {
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// For primitives (number, boolean, string), access the property
|
|
734
|
+
// This handles cases like "2.val" -> undefined, or potentially "hello".length -> 5
|
|
735
|
+
// But we need to handle chained access like "2.val.something"
|
|
736
|
+
const propParts = this.splitPropertyPath(propPart);
|
|
737
|
+
let result: any = baseValue;
|
|
738
|
+
|
|
739
|
+
for (const prop of propParts) {
|
|
740
|
+
if (result === null || result === undefined) {
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
// Handle bracket notation within the property path
|
|
744
|
+
if (prop.includes('[')) {
|
|
745
|
+
result = this.accessPropertyWithBrackets(result, prop);
|
|
746
|
+
} else {
|
|
747
|
+
result = result[prop];
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Split a property path like "val.something" into ["val", "something"]
|
|
756
|
+
* Handles bracket notation like "arr[0].value"
|
|
757
|
+
*/
|
|
758
|
+
private splitPropertyPath(path: string): string[] {
|
|
759
|
+
const parts: string[] = [];
|
|
760
|
+
let current = '';
|
|
761
|
+
let inBracket = false;
|
|
762
|
+
|
|
763
|
+
for (let i = 0; i < path.length; i++) {
|
|
764
|
+
const ch = path.charAt(i);
|
|
765
|
+
if (ch === '[') {
|
|
766
|
+
if (current.length > 0) {
|
|
767
|
+
parts.push(current);
|
|
768
|
+
current = '';
|
|
769
|
+
}
|
|
770
|
+
inBracket = true;
|
|
771
|
+
current += ch;
|
|
772
|
+
} else if (ch === ']') {
|
|
773
|
+
current += ch;
|
|
774
|
+
inBracket = false;
|
|
775
|
+
parts.push(current);
|
|
776
|
+
current = '';
|
|
777
|
+
} else if (ch === '.' && !inBracket) {
|
|
778
|
+
if (current.length > 0) {
|
|
779
|
+
parts.push(current);
|
|
780
|
+
current = '';
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
current += ch;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (current.length > 0) {
|
|
788
|
+
parts.push(current);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return parts;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Access a property that may contain bracket notation like "[0]" or '["key"]'
|
|
796
|
+
*/
|
|
797
|
+
private accessPropertyWithBrackets(obj: any, prop: string): any {
|
|
798
|
+
// Handle bracket notation like "[0]" or '["key"]'
|
|
799
|
+
const bracketMatch = prop.match(/^\[(.+)\]$/);
|
|
800
|
+
if (bracketMatch) {
|
|
801
|
+
let key = bracketMatch[1];
|
|
802
|
+
// Remove quotes if present
|
|
803
|
+
if ((key.startsWith('"') && key.endsWith('"')) ||
|
|
804
|
+
(key.startsWith("'") && key.endsWith("'"))) {
|
|
805
|
+
key = key.substring(1, key.length - 1);
|
|
806
|
+
}
|
|
807
|
+
// Try to parse as number
|
|
808
|
+
const numKey = parseInt(key);
|
|
809
|
+
if (!isNaN(numKey)) {
|
|
810
|
+
return obj[numKey];
|
|
811
|
+
}
|
|
812
|
+
return obj[key];
|
|
813
|
+
}
|
|
814
|
+
return obj[prop];
|
|
815
|
+
}
|
|
816
|
+
|
|
636
817
|
private applyTernaryOperation(operator: Operation, v1: any, v2: any, v3: any): ExpressionToken {
|
|
637
818
|
let op: TernaryOperator | undefined =
|
|
638
819
|
ExpressionEvaluator.TERNARY_OPERATORS_MAP.get(operator);
|
|
@@ -736,6 +917,120 @@ export class ExpressionEvaluator {
|
|
|
736
917
|
|
|
737
918
|
return LiteralTokenValueExtractor.INSTANCE.getValueFromExtractors(path, valuesMap);
|
|
738
919
|
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Build a path string from a path Expression, without parentheses.
|
|
923
|
+
* E.g., Expression for "a.(b.c)" returns "a.b.c"
|
|
924
|
+
*/
|
|
925
|
+
private buildPathString(expr: Expression): string {
|
|
926
|
+
const ops = expr.getOperationsArray();
|
|
927
|
+
const tokens = expr.getTokensArray();
|
|
928
|
+
|
|
929
|
+
// Leaf expression - just return the token string
|
|
930
|
+
if (ops.length === 0) {
|
|
931
|
+
if (tokens.length === 1) {
|
|
932
|
+
const token = tokens[0];
|
|
933
|
+
if (token instanceof Expression) {
|
|
934
|
+
return this.buildPathString(token);
|
|
935
|
+
}
|
|
936
|
+
// For ExpressionTokenValue, use getExpression() not toString()
|
|
937
|
+
// (toString() returns "expr: value" format which is wrong for paths)
|
|
938
|
+
return this.getTokenExpressionString(token);
|
|
939
|
+
}
|
|
940
|
+
return expr.getExpression() || '';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Binary expression - build path from tokens and operators
|
|
944
|
+
// With push() order: tokens[0]=right, tokens[1]=left
|
|
945
|
+
if (tokens.length >= 2 && ops.length >= 1) {
|
|
946
|
+
const right = tokens[0];
|
|
947
|
+
const left = tokens[1];
|
|
948
|
+
const op = ops[0];
|
|
949
|
+
|
|
950
|
+
const leftStr = left instanceof Expression ? this.buildPathString(left) : this.getTokenExpressionString(left);
|
|
951
|
+
const rightStr = right instanceof Expression ? this.buildPathString(right) : this.getTokenExpressionString(right);
|
|
952
|
+
|
|
953
|
+
if (op === Operation.OBJECT_OPERATOR) {
|
|
954
|
+
return leftStr + '.' + rightStr;
|
|
955
|
+
} else if (op === Operation.ARRAY_OPERATOR) {
|
|
956
|
+
return leftStr + '[' + rightStr + ']';
|
|
957
|
+
} else if (op === Operation.ARRAY_RANGE_INDEX_OPERATOR) {
|
|
958
|
+
return leftStr + '..' + rightStr;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Fallback to toString() with parens stripped
|
|
963
|
+
return this.stripOuterParens(expr.toString());
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Get the expression string from a token, handling ExpressionTokenValue specially.
|
|
968
|
+
*/
|
|
969
|
+
private getTokenExpressionString(token: ExpressionToken): string {
|
|
970
|
+
// For ExpressionTokenValue, use getExpression() not toString()
|
|
971
|
+
// because toString() returns "expr: value" format
|
|
972
|
+
if (token instanceof ExpressionTokenValue) {
|
|
973
|
+
return token.getExpression();
|
|
974
|
+
}
|
|
975
|
+
return token.getExpression();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Strip outer parentheses from a string if they exist.
|
|
980
|
+
* E.g., "(Context.obj)" -> "Context.obj"
|
|
981
|
+
*/
|
|
982
|
+
private stripOuterParens(str: string): string {
|
|
983
|
+
if (str.length >= 2 && str.charAt(0) === '(' && str.charAt(str.length - 1) === ')') {
|
|
984
|
+
// Count parentheses to ensure we only strip matching outer parens
|
|
985
|
+
let depth = 0;
|
|
986
|
+
for (let i = 0; i < str.length; i++) {
|
|
987
|
+
if (str.charAt(i) === '(') depth++;
|
|
988
|
+
else if (str.charAt(i) === ')') depth--;
|
|
989
|
+
// If depth becomes 0 before the last char, the outer parens don't match
|
|
990
|
+
if (depth === 0 && i < str.length - 1) {
|
|
991
|
+
return str; // Don't strip, the parens don't match
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return str.substring(1, str.length - 1);
|
|
995
|
+
}
|
|
996
|
+
return str;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Check if an Expression is a path component (identifier, OBJECT_OPERATOR, or ARRAY_OPERATOR).
|
|
1001
|
+
* Path components should use toString() for path building, not be evaluated as values.
|
|
1002
|
+
* Expressions with other operators (like +, -, etc.) should be evaluated.
|
|
1003
|
+
*/
|
|
1004
|
+
private isPathExpression(expr: Expression): boolean {
|
|
1005
|
+
const ops = expr.getOperationsArray();
|
|
1006
|
+
|
|
1007
|
+
// No operations = leaf identifier - use toString()
|
|
1008
|
+
if (ops.length === 0) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Check if all operations are path-related (OBJECT_OPERATOR or ARRAY_OPERATOR)
|
|
1013
|
+
for (const op of ops) {
|
|
1014
|
+
if (op !== Operation.OBJECT_OPERATOR &&
|
|
1015
|
+
op !== Operation.ARRAY_OPERATOR &&
|
|
1016
|
+
op !== Operation.ARRAY_RANGE_INDEX_OPERATOR) {
|
|
1017
|
+
// Has non-path operator - needs evaluation
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Also check nested expressions in tokens
|
|
1023
|
+
const tokens = expr.getTokensArray();
|
|
1024
|
+
for (const token of tokens) {
|
|
1025
|
+
if (token instanceof Expression) {
|
|
1026
|
+
if (!this.isPathExpression(token)) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
739
1034
|
}
|
|
740
1035
|
|
|
741
1036
|
|