@halleyassist/rule-parser 1.0.26 → 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.
@@ -2752,10 +2752,14 @@ class ErrorAnalyzer {
2752
2752
 
2753
2753
  // Get snippet (last 50 chars or the whole input if shorter)
2754
2754
  const snippetStart = Math.max(0, trimmedInput.length - 50);
2755
- const snippet = (snippetStart > 0 ? '...' : '') + trimmedInput.substring(snippetStart);
2755
+ const snippetEnd = trimmedInput.length;
2756
+ let snippet = this._buildSnippet(trimmedInput, snippetStart, snippetEnd);
2756
2757
 
2757
2758
  // Analyze the error pattern
2758
2759
  const errorInfo = this._detectErrorPattern(trimmedInput, position, snippet);
2760
+ this._enrichBadFunctionCallError(trimmedInput, position, errorInfo, (value) => {
2761
+ snippet = value;
2762
+ }, snippetStart, snippetEnd);
2759
2763
 
2760
2764
  return new RuleParseError(
2761
2765
  errorInfo.code,
@@ -2781,12 +2785,13 @@ class ErrorAnalyzer {
2781
2785
  // Get snippet around error position
2782
2786
  const snippetStart = Math.max(0, position.offset - 20);
2783
2787
  const snippetEnd = Math.min(input.length, position.offset + 30);
2784
- const snippet = (snippetStart > 0 ? '...' : '') +
2785
- input.substring(snippetStart, snippetEnd) +
2786
- (snippetEnd < input.length ? '...' : '');
2788
+ let snippet = this._buildSnippet(input, snippetStart, snippetEnd);
2787
2789
 
2788
2790
  // Analyze what was expected to determine error type using failureTree
2789
2791
  const errorInfo = this._detectErrorFromFailureTree(input, position, expected, found, failureTree);
2792
+ this._enrichBadFunctionCallError(input, position, errorInfo, (value) => {
2793
+ snippet = value;
2794
+ }, snippetStart, snippetEnd);
2790
2795
 
2791
2796
  return new RuleParseError(
2792
2797
  errorInfo.code,
@@ -3300,6 +3305,109 @@ class ErrorAnalyzer {
3300
3305
  };
3301
3306
  }
3302
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
+
3303
3411
  /**
3304
3412
  * Check if string is properly terminated
3305
3413
  * @private
@@ -3671,12 +3779,6 @@ class RuleParseError extends Error {
3671
3779
  if (this.hint) {
3672
3780
  msg += ` Hint: ${this.hint}\n`;
3673
3781
  }
3674
- if (this.found) {
3675
- msg += ` Found: ${this.found}\n`;
3676
- }
3677
- if (this.expected && this.expected.length) {
3678
- msg += ` Expected: ${this.expected.join(', ')}`;
3679
- }
3680
3782
  return msg;
3681
3783
  }
3682
3784
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-parser",
3
- "version": "1.0.26",
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",
@@ -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