@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.
@@ -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
- objTokens.push(
567
- new ExpressionTokenValue(
568
- token.toString(),
569
- this.evaluateExpression(token, valuesMap),
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
- token = hasMoreTokens() ? popToken() : undefined;
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
- objTokens.push(
581
- new ExpressionTokenValue(
582
- token.toString(),
583
- this.evaluateExpression(token, valuesMap),
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
- let str: string = objToken instanceof ExpressionTokenValue
611
- ? objToken.getTokenValue()
612
- : objToken.toString();
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
- const tokenVal = objToken instanceof ExpressionTokenValue
618
- ? objToken.getTokenValue()
619
- : objToken.toString();
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
- v = str;
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