@halleyassist/rule-parser 1.0.25 → 1.0.27

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.
@@ -1949,6 +1949,31 @@ const LogicalOperators = {
1949
1949
  '||': 'Or',
1950
1950
  'OR': 'Or'
1951
1951
  };
1952
+ const ILReservedNames = new Set([
1953
+ 'Value',
1954
+ 'Array',
1955
+ 'ArrayIn',
1956
+ 'Between',
1957
+ 'Not',
1958
+ 'And',
1959
+ 'Or',
1960
+ 'Gt',
1961
+ 'Lt',
1962
+ 'Gte',
1963
+ 'Lte',
1964
+ 'Eq',
1965
+ 'Neq',
1966
+ 'MathAdd',
1967
+ 'MathSub',
1968
+ 'MathDiv',
1969
+ 'MathMul',
1970
+ 'MathMod',
1971
+ 'Default',
1972
+ 'TimePeriodConst',
1973
+ 'TimePeriodConstAgo',
1974
+ 'TimePeriodBetween',
1975
+ 'TimePeriodBetweenAgo'
1976
+ ]);
1952
1977
  const DOW_MAP = {
1953
1978
  'MON': 'MONDAY',
1954
1979
  'TUE': 'TUESDAY',
@@ -2609,6 +2634,29 @@ class RuleParser {
2609
2634
  throw new Error(`unknown type of expression ${ eInner.type }`);
2610
2635
  }
2611
2636
  }
2637
+ static _collectFunctions(il, names) {
2638
+ if (!Array.isArray(il) || il.length === 0) {
2639
+ return;
2640
+ }
2641
+ const [head, ...tail] = il;
2642
+ if (typeof head === 'string') {
2643
+ if (!ILReservedNames.has(head)) {
2644
+ names.add(head);
2645
+ }
2646
+ for (const child of tail) {
2647
+ RuleParser._collectFunctions(child, names);
2648
+ }
2649
+ return;
2650
+ }
2651
+ for (const child of il) {
2652
+ RuleParser._collectFunctions(child, names);
2653
+ }
2654
+ }
2655
+ static getFunctions(il) {
2656
+ const names = new Set();
2657
+ RuleParser._collectFunctions(il, names);
2658
+ return [...names];
2659
+ }
2612
2660
  static toIL(txt) {
2613
2661
  try {
2614
2662
  const ast = RuleParser.toAst(txt);
@@ -2704,10 +2752,14 @@ class ErrorAnalyzer {
2704
2752
 
2705
2753
  // Get snippet (last 50 chars or the whole input if shorter)
2706
2754
  const snippetStart = Math.max(0, trimmedInput.length - 50);
2707
- const snippet = (snippetStart > 0 ? '...' : '') + trimmedInput.substring(snippetStart);
2755
+ const snippetEnd = trimmedInput.length;
2756
+ let snippet = this._buildSnippet(trimmedInput, snippetStart, snippetEnd);
2708
2757
 
2709
2758
  // Analyze the error pattern
2710
2759
  const errorInfo = this._detectErrorPattern(trimmedInput, position, snippet);
2760
+ this._enrichBadFunctionCallError(trimmedInput, position, errorInfo, (value) => {
2761
+ snippet = value;
2762
+ }, snippetStart, snippetEnd);
2711
2763
 
2712
2764
  return new RuleParseError(
2713
2765
  errorInfo.code,
@@ -2733,12 +2785,13 @@ class ErrorAnalyzer {
2733
2785
  // Get snippet around error position
2734
2786
  const snippetStart = Math.max(0, position.offset - 20);
2735
2787
  const snippetEnd = Math.min(input.length, position.offset + 30);
2736
- const snippet = (snippetStart > 0 ? '...' : '') +
2737
- input.substring(snippetStart, snippetEnd) +
2738
- (snippetEnd < input.length ? '...' : '');
2788
+ let snippet = this._buildSnippet(input, snippetStart, snippetEnd);
2739
2789
 
2740
2790
  // Analyze what was expected to determine error type using failureTree
2741
2791
  const errorInfo = this._detectErrorFromFailureTree(input, position, expected, found, failureTree);
2792
+ this._enrichBadFunctionCallError(input, position, errorInfo, (value) => {
2793
+ snippet = value;
2794
+ }, snippetStart, snippetEnd);
2742
2795
 
2743
2796
  return new RuleParseError(
2744
2797
  errorInfo.code,
@@ -3252,6 +3305,109 @@ class ErrorAnalyzer {
3252
3305
  };
3253
3306
  }
3254
3307
 
3308
+ static _enrichBadFunctionCallError(input, position, errorInfo, setSnippet, snippetStart, snippetEnd) {
3309
+ if (!errorInfo || errorInfo.code !== "BAD_FUNCTION_CALL") {
3310
+ return;
3311
+ }
3312
+
3313
+ const functionContext = this._findFunctionContext(input, position.offset);
3314
+ errorInfo.hint = this._getBadFunctionCallHint(input, position, functionContext);
3315
+
3316
+ if (!functionContext) {
3317
+ return;
3318
+ }
3319
+
3320
+ errorInfo.message = `Invalid function call syntax for ${functionContext.name}.`;
3321
+ setSnippet(this._buildBadFunctionCallSnippet(input, functionContext, snippetStart, snippetEnd));
3322
+ }
3323
+
3324
+ static _buildSnippet(input, start, end) {
3325
+ return (start > 0 ? '...' : '') +
3326
+ input.substring(start, end) +
3327
+ (end < input.length ? '...' : '');
3328
+ }
3329
+
3330
+ static _getBadFunctionCallHint(input, position, functionContext) {
3331
+ const genericHint = "Function calls must have matching parentheses, e.g. func() or func(arg1, arg2).";
3332
+
3333
+ if (!functionContext || !this._isArgumentRelatedFunctionCallError(input, position, functionContext)) {
3334
+ return genericHint;
3335
+ }
3336
+
3337
+ return "Function calls must have matching parentheses, e.g. func() or func(arg1, arg2). Please check function arguments for invalid syntax.";
3338
+ }
3339
+
3340
+ static _isArgumentRelatedFunctionCallError(input, position, functionContext) {
3341
+ const endOffset = Math.max(functionContext.openParenIndex + 1, Math.min(position.offset, input.length));
3342
+ const argumentText = input.substring(functionContext.openParenIndex + 1, endOffset).trim();
3343
+ return argumentText.length > 0;
3344
+ }
3345
+
3346
+ static _buildBadFunctionCallSnippet(input, functionContext, snippetStart, snippetEnd) {
3347
+ if (snippetStart <= functionContext.start) {
3348
+ return this._buildSnippet(input, snippetStart, snippetEnd);
3349
+ }
3350
+
3351
+ const functionPrefix = input.substring(functionContext.start, functionContext.openParenIndex + 1);
3352
+ const suffix = input.substring(snippetStart, snippetEnd);
3353
+ return functionPrefix + '...' + suffix + (snippetEnd < input.length ? '...' : '');
3354
+ }
3355
+
3356
+ static _findFunctionContext(input, offset) {
3357
+ const scanLimit = Math.max(0, Math.min(typeof offset === 'number' ? offset : input.length, input.length));
3358
+ const stack = [];
3359
+ let inString = false;
3360
+
3361
+ for (let i = 0; i < scanLimit; i++) {
3362
+ const char = input[i];
3363
+
3364
+ if (char === '"') {
3365
+ if (!inString) {
3366
+ inString = true;
3367
+ } else {
3368
+ let backslashCount = 0;
3369
+ let j = i - 1;
3370
+ while (j >= 0 && input[j] === '\\') {
3371
+ backslashCount++;
3372
+ j--;
3373
+ }
3374
+ if (backslashCount % 2 === 0) {
3375
+ inString = false;
3376
+ }
3377
+ }
3378
+ continue;
3379
+ }
3380
+
3381
+ if (inString) {
3382
+ continue;
3383
+ }
3384
+
3385
+ if (char === '(') {
3386
+ const prefix = input.substring(0, i);
3387
+ const match = /([a-zA-Z_][a-zA-Z0-9_]*)\s*$/.exec(prefix);
3388
+ if (match) {
3389
+ stack.push({
3390
+ name: match[1],
3391
+ start: i - match[1].length,
3392
+ openParenIndex: i
3393
+ });
3394
+ } else {
3395
+ stack.push(null);
3396
+ }
3397
+ } else if (char === ')' && stack.length > 0) {
3398
+ stack.pop();
3399
+ }
3400
+ }
3401
+
3402
+ for (let i = stack.length - 1; i >= 0; i--) {
3403
+ if (stack[i]) {
3404
+ return stack[i];
3405
+ }
3406
+ }
3407
+
3408
+ return null;
3409
+ }
3410
+
3255
3411
  /**
3256
3412
  * Check if string is properly terminated
3257
3413
  * @private
@@ -3623,12 +3779,6 @@ class RuleParseError extends Error {
3623
3779
  if (this.hint) {
3624
3780
  msg += ` Hint: ${this.hint}\n`;
3625
3781
  }
3626
- if (this.found) {
3627
- msg += ` Found: ${this.found}\n`;
3628
- }
3629
- if (this.expected && this.expected.length) {
3630
- msg += ` Expected: ${this.expected.join(', ')}`;
3631
- }
3632
3782
  return msg;
3633
3783
  }
3634
3784
 
package/index.d.ts CHANGED
@@ -127,12 +127,24 @@ export type TimePeriodExpression =
127
127
  | TimePeriodBetween
128
128
  | TimePeriodBetweenAgo;
129
129
 
130
+ /**
131
+ * Array expression used for dynamic `IN (...)` arguments
132
+ */
133
+ export type ArrayExpression = ['Array', ...ILExpression[]];
134
+
135
+ /**
136
+ * Array membership expression
137
+ */
138
+ export type ArrayInExpression = ['ArrayIn', ILExpression, ILExpression];
139
+
130
140
  /**
131
141
  * Forward declaration for ILExpression to handle recursive types
132
142
  */
133
143
  export type ILExpression =
134
144
  | ValueExpression
135
145
  | TimePeriodExpression
146
+ | ArrayExpression
147
+ | ArrayInExpression
136
148
  | FunctionCall
137
149
  | ComparisonExpression
138
150
  | LogicalExpression
@@ -207,6 +219,13 @@ declare class RuleParser {
207
219
  * @throws {RuleParseError} If the rule string is invalid
208
220
  */
209
221
  static toIL(txt: string): ILExpression;
222
+
223
+ /**
224
+ * Collect unique function names referenced in an IL expression
225
+ * @param il - The IL expression to inspect
226
+ * @returns Unique function names in first-seen order
227
+ */
228
+ static getFunctions(il: ILExpression): string[];
210
229
  }
211
230
 
212
231
  export default RuleParser;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-parser",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleParser.production.js",
6
6
  "browser": "./dist/rule-parser.browser.js",
package/src/RuleParser.js CHANGED
@@ -34,6 +34,32 @@ const LogicalOperators = {
34
34
  "OR": 'Or',
35
35
  }
36
36
 
37
+ const ILReservedNames = new Set([
38
+ 'Value',
39
+ 'Array',
40
+ 'ArrayIn',
41
+ 'Between',
42
+ 'Not',
43
+ 'And',
44
+ 'Or',
45
+ 'Gt',
46
+ 'Lt',
47
+ 'Gte',
48
+ 'Lte',
49
+ 'Eq',
50
+ 'Neq',
51
+ 'MathAdd',
52
+ 'MathSub',
53
+ 'MathDiv',
54
+ 'MathMul',
55
+ 'MathMod',
56
+ 'Default',
57
+ 'TimePeriodConst',
58
+ 'TimePeriodConstAgo',
59
+ 'TimePeriodBetween',
60
+ 'TimePeriodBetweenAgo'
61
+ ])
62
+
37
63
  // Map abbreviations to canonical uppercase full form
38
64
  const DOW_MAP = {
39
65
  'MON': 'MONDAY',
@@ -623,6 +649,32 @@ class RuleParser {
623
649
  throw new Error(`unknown type of expression ${eInner.type}`)
624
650
  }
625
651
  }
652
+ static _collectFunctions(il, names){
653
+ if(!Array.isArray(il) || il.length === 0){
654
+ return
655
+ }
656
+
657
+ const [head, ...tail] = il
658
+
659
+ if(typeof head === 'string'){
660
+ if(!ILReservedNames.has(head)){
661
+ names.add(head)
662
+ }
663
+ for(const child of tail){
664
+ RuleParser._collectFunctions(child, names)
665
+ }
666
+ return
667
+ }
668
+
669
+ for(const child of il){
670
+ RuleParser._collectFunctions(child, names)
671
+ }
672
+ }
673
+ static getFunctions(il){
674
+ const names = new Set()
675
+ RuleParser._collectFunctions(il, names)
676
+ return [...names]
677
+ }
626
678
  static toIL(txt){
627
679
  try {
628
680
  const ast = RuleParser.toAst(txt)
@@ -34,6 +34,32 @@ const LogicalOperators = {
34
34
  "OR": 'Or',
35
35
  }
36
36
 
37
+ const ILReservedNames = new Set([
38
+ 'Value',
39
+ 'Array',
40
+ 'ArrayIn',
41
+ 'Between',
42
+ 'Not',
43
+ 'And',
44
+ 'Or',
45
+ 'Gt',
46
+ 'Lt',
47
+ 'Gte',
48
+ 'Lte',
49
+ 'Eq',
50
+ 'Neq',
51
+ 'MathAdd',
52
+ 'MathSub',
53
+ 'MathDiv',
54
+ 'MathMul',
55
+ 'MathMod',
56
+ 'Default',
57
+ 'TimePeriodConst',
58
+ 'TimePeriodConstAgo',
59
+ 'TimePeriodBetween',
60
+ 'TimePeriodBetweenAgo'
61
+ ])
62
+
37
63
  // Map abbreviations to canonical uppercase full form
38
64
  const DOW_MAP = {
39
65
  'MON': 'MONDAY',
@@ -623,6 +649,32 @@ class RuleParser {
623
649
  throw new Error(`unknown type of expression ${eInner.type}`)
624
650
  }
625
651
  }
652
+ static _collectFunctions(il, names){
653
+ if(!Array.isArray(il) || il.length === 0){
654
+ return
655
+ }
656
+
657
+ const [head, ...tail] = il
658
+
659
+ if(typeof head === 'string'){
660
+ if(!ILReservedNames.has(head)){
661
+ names.add(head)
662
+ }
663
+ for(const child of tail){
664
+ RuleParser._collectFunctions(child, names)
665
+ }
666
+ return
667
+ }
668
+
669
+ for(const child of il){
670
+ RuleParser._collectFunctions(child, names)
671
+ }
672
+ }
673
+ static getFunctions(il){
674
+ const names = new Set()
675
+ RuleParser._collectFunctions(il, names)
676
+ return [...names]
677
+ }
626
678
  static toIL(txt){
627
679
  try {
628
680
  const ast = RuleParser.toAst(txt)
@@ -44,10 +44,14 @@ class ErrorAnalyzer {
44
44
 
45
45
  // Get snippet (last 50 chars or the whole input if shorter)
46
46
  const snippetStart = Math.max(0, trimmedInput.length - 50);
47
- const snippet = (snippetStart > 0 ? '...' : '') + trimmedInput.substring(snippetStart);
47
+ const snippetEnd = trimmedInput.length;
48
+ let snippet = this._buildSnippet(trimmedInput, snippetStart, snippetEnd);
48
49
 
49
50
  // Analyze the error pattern
50
51
  const errorInfo = this._detectErrorPattern(trimmedInput, position, snippet);
52
+ this._enrichBadFunctionCallError(trimmedInput, position, errorInfo, (value) => {
53
+ snippet = value;
54
+ }, snippetStart, snippetEnd);
51
55
 
52
56
  return new RuleParseError(
53
57
  errorInfo.code,
@@ -73,12 +77,13 @@ class ErrorAnalyzer {
73
77
  // Get snippet around error position
74
78
  const snippetStart = Math.max(0, position.offset - 20);
75
79
  const snippetEnd = Math.min(input.length, position.offset + 30);
76
- const snippet = (snippetStart > 0 ? '...' : '') +
77
- input.substring(snippetStart, snippetEnd) +
78
- (snippetEnd < input.length ? '...' : '');
80
+ let snippet = this._buildSnippet(input, snippetStart, snippetEnd);
79
81
 
80
82
  // Analyze what was expected to determine error type using failureTree
81
83
  const errorInfo = this._detectErrorFromFailureTree(input, position, expected, found, failureTree);
84
+ this._enrichBadFunctionCallError(input, position, errorInfo, (value) => {
85
+ snippet = value;
86
+ }, snippetStart, snippetEnd);
82
87
 
83
88
  return new RuleParseError(
84
89
  errorInfo.code,
@@ -592,6 +597,109 @@ class ErrorAnalyzer {
592
597
  };
593
598
  }
594
599
 
600
+ static _enrichBadFunctionCallError(input, position, errorInfo, setSnippet, snippetStart, snippetEnd) {
601
+ if (!errorInfo || errorInfo.code !== "BAD_FUNCTION_CALL") {
602
+ return;
603
+ }
604
+
605
+ const functionContext = this._findFunctionContext(input, position.offset);
606
+ errorInfo.hint = this._getBadFunctionCallHint(input, position, functionContext);
607
+
608
+ if (!functionContext) {
609
+ return;
610
+ }
611
+
612
+ errorInfo.message = `Invalid function call syntax for ${functionContext.name}.`;
613
+ setSnippet(this._buildBadFunctionCallSnippet(input, functionContext, snippetStart, snippetEnd));
614
+ }
615
+
616
+ static _buildSnippet(input, start, end) {
617
+ return (start > 0 ? '...' : '') +
618
+ input.substring(start, end) +
619
+ (end < input.length ? '...' : '');
620
+ }
621
+
622
+ static _getBadFunctionCallHint(input, position, functionContext) {
623
+ const genericHint = "Function calls must have matching parentheses, e.g. func() or func(arg1, arg2).";
624
+
625
+ if (!functionContext || !this._isArgumentRelatedFunctionCallError(input, position, functionContext)) {
626
+ return genericHint;
627
+ }
628
+
629
+ return "Function calls must have matching parentheses, e.g. func() or func(arg1, arg2). Please check function arguments for invalid syntax.";
630
+ }
631
+
632
+ static _isArgumentRelatedFunctionCallError(input, position, functionContext) {
633
+ const endOffset = Math.max(functionContext.openParenIndex + 1, Math.min(position.offset, input.length));
634
+ const argumentText = input.substring(functionContext.openParenIndex + 1, endOffset).trim();
635
+ return argumentText.length > 0;
636
+ }
637
+
638
+ static _buildBadFunctionCallSnippet(input, functionContext, snippetStart, snippetEnd) {
639
+ if (snippetStart <= functionContext.start) {
640
+ return this._buildSnippet(input, snippetStart, snippetEnd);
641
+ }
642
+
643
+ const functionPrefix = input.substring(functionContext.start, functionContext.openParenIndex + 1);
644
+ const suffix = input.substring(snippetStart, snippetEnd);
645
+ return functionPrefix + '...' + suffix + (snippetEnd < input.length ? '...' : '');
646
+ }
647
+
648
+ static _findFunctionContext(input, offset) {
649
+ const scanLimit = Math.max(0, Math.min(typeof offset === 'number' ? offset : input.length, input.length));
650
+ const stack = [];
651
+ let inString = false;
652
+
653
+ for (let i = 0; i < scanLimit; i++) {
654
+ const char = input[i];
655
+
656
+ if (char === '"') {
657
+ if (!inString) {
658
+ inString = true;
659
+ } else {
660
+ let backslashCount = 0;
661
+ let j = i - 1;
662
+ while (j >= 0 && input[j] === '\\') {
663
+ backslashCount++;
664
+ j--;
665
+ }
666
+ if (backslashCount % 2 === 0) {
667
+ inString = false;
668
+ }
669
+ }
670
+ continue;
671
+ }
672
+
673
+ if (inString) {
674
+ continue;
675
+ }
676
+
677
+ if (char === '(') {
678
+ const prefix = input.substring(0, i);
679
+ const match = /([a-zA-Z_][a-zA-Z0-9_]*)\s*$/.exec(prefix);
680
+ if (match) {
681
+ stack.push({
682
+ name: match[1],
683
+ start: i - match[1].length,
684
+ openParenIndex: i
685
+ });
686
+ } else {
687
+ stack.push(null);
688
+ }
689
+ } else if (char === ')' && stack.length > 0) {
690
+ stack.pop();
691
+ }
692
+ }
693
+
694
+ for (let i = stack.length - 1; i >= 0; i--) {
695
+ if (stack[i]) {
696
+ return stack[i];
697
+ }
698
+ }
699
+
700
+ return null;
701
+ }
702
+
595
703
  /**
596
704
  * Check if string is properly terminated
597
705
  * @private
@@ -37,12 +37,6 @@ class RuleParseError extends Error {
37
37
  if (this.hint) {
38
38
  msg += ` Hint: ${this.hint}\n`;
39
39
  }
40
- if (this.found) {
41
- msg += ` Found: ${this.found}\n`;
42
- }
43
- if (this.expected && this.expected.length) {
44
- msg += ` Expected: ${this.expected.join(', ')}`;
45
- }
46
40
  return msg;
47
41
  }
48
42