@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.
- package/dist/rule-parser.browser.js +160 -10
- package/index.d.ts +19 -0
- package/package.json +1 -1
- package/src/RuleParser.js +52 -0
- package/src/RuleParser.production.js +52 -0
- package/src/errors/ErrorAnalyzer.js +112 -4
- package/src/errors/RuleParseError.js +0 -6
|
@@ -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
|
|
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
|
-
|
|
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
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
|
|
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
|
-
|
|
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
|
|