@genesislcap/mock-server 15.59.0 → 15.61.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/README.md +287 -40
- package/dist/db/criteria.d.ts +28 -9
- package/dist/db/criteria.js +856 -258
- package/dist/db/criteriaDialect.d.ts +6 -0
- package/dist/db/criteriaDialect.js +441 -0
- package/dist/db/criteriaTokens.d.ts +12 -0
- package/dist/db/criteriaTokens.js +53 -0
- package/dist/db/groovySyntax.d.ts +26 -0
- package/dist/db/groovySyntax.js +481 -0
- package/dist/db/groovyValidator.d.ts +11 -0
- package/dist/db/groovyValidator.js +523 -0
- package/dist/db/gsfExpr.d.ts +21 -0
- package/dist/db/gsfExpr.js +333 -0
- package/dist/db/identity.d.ts +2 -1
- package/dist/db/identity.js +24 -3
- package/dist/db/legacyCriteria.d.ts +7 -0
- package/dist/db/legacyCriteria.js +280 -0
- package/dist/db/ordering.d.ts +2 -0
- package/dist/db/ordering.js +24 -3
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +34 -3
- package/dist/db/store.d.ts +7 -0
- package/dist/db/store.js +55 -7
- package/dist/db/values.d.ts +19 -0
- package/dist/db/values.js +243 -0
- package/dist/handlers/commitEvent.js +3 -2
- package/dist/handlers/dataLogon.d.ts +2 -1
- package/dist/handlers/dataLogon.js +108 -51
- package/dist/handlers/eventValidation.d.ts +2 -1
- package/dist/handlers/eventValidation.js +33 -6
- package/dist/handlers/requestReply.js +43 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol/connection.d.ts +1 -1
- package/dist/server.js +15 -8
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +979 -290
- package/src/db/criteriaDialect.ts +487 -0
- package/src/db/criteriaTokens.ts +72 -0
- package/src/db/groovySyntax.ts +500 -0
- package/src/db/groovyValidator.ts +546 -0
- package/src/db/gsfExpr.ts +392 -0
- package/src/db/identity.ts +22 -4
- package/src/db/legacyCriteria.ts +301 -0
- package/src/db/ordering.ts +27 -3
- package/src/db/schema.ts +43 -8
- package/src/db/store.ts +65 -7
- package/src/db/values.ts +247 -0
- package/src/handlers/commitEvent.ts +3 -2
- package/src/handlers/dataLogon.ts +134 -56
- package/src/handlers/eventValidation.ts +40 -13
- package/src/handlers/requestReply.ts +47 -14
- package/src/index.ts +6 -1
- package/src/protocol/connection.ts +3 -4
- package/src/server.ts +16 -15
- package/src/types.ts +7 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// Which CRITERIA_MATCH strings real GSF refuses to compile, and the text it
|
|
2
|
+
// refuses them with.
|
|
3
|
+
//
|
|
4
|
+
// GSF parses a criteria as Groovy before anything else
|
|
5
|
+
// (CriteriaFilterBuilder.validateAndParseCriteriaOptions:
|
|
6
|
+
// AstBuilder().buildFromString(criteria), GSF v8.15.29, Groovy 3.0.25), and a
|
|
7
|
+
// parse failure becomes INVALID_CRITERIA with the compiler's message. This
|
|
8
|
+
// engine's criteria grammar (criteria.ts) is much smaller than Groovy, so its
|
|
9
|
+
// own parse failures can't be taken for Groovy's: `ENABLED`, `NAME in ['A']`
|
|
10
|
+
// or arithmetic are valid Groovy it doesn't model. This check answers only
|
|
11
|
+
// for the mistakes it can pin down as Groovy does — a dangling operator, an
|
|
12
|
+
// unclosed or unopened parenthesis, an operator where an operand must start,
|
|
13
|
+
// an unterminated string — and gives up (undefined) on anything else it
|
|
14
|
+
// doesn't understand, so a criteria is refused only when Groovy would refuse
|
|
15
|
+
// it too. The messages and positions follow what Groovy 3.0.25's AstBuilder
|
|
16
|
+
// reports for those cases (checked by running it; test/groovySyntax.test.ts pins
|
|
17
|
+
// them), though ANTLR names a different token in a few of them.
|
|
18
|
+
|
|
19
|
+
export interface GroovySyntaxError {
|
|
20
|
+
// "Unexpected input: '<EOF>'", "Unexpected character: '\''", ...
|
|
21
|
+
message: string;
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type TokenKind = 'name' | 'literal' | 'operator' | 'open' | 'close' | 'comma' | 'semicolon';
|
|
27
|
+
|
|
28
|
+
export interface Token {
|
|
29
|
+
kind: TokenKind;
|
|
30
|
+
text: string;
|
|
31
|
+
offset: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Thrown when the criteria is outside what this check understands: no verdict.
|
|
35
|
+
class GiveUp extends Error {}
|
|
36
|
+
|
|
37
|
+
// Thrown for a syntax error Groovy reports the same way.
|
|
38
|
+
class Refused extends Error {
|
|
39
|
+
readonly text: string;
|
|
40
|
+
readonly offset: number;
|
|
41
|
+
readonly kind: 'input' | 'character';
|
|
42
|
+
|
|
43
|
+
constructor(text: string, offset: number, kind: 'input' | 'character' = 'input') {
|
|
44
|
+
super(text);
|
|
45
|
+
this.text = text;
|
|
46
|
+
this.offset = offset;
|
|
47
|
+
this.kind = kind;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Groovy's operators, longest first so the scanner takes `===` before `==`.
|
|
52
|
+
const OPERATORS = [
|
|
53
|
+
'>>>=',
|
|
54
|
+
'===',
|
|
55
|
+
'!==',
|
|
56
|
+
'<=>',
|
|
57
|
+
'==~',
|
|
58
|
+
'**=',
|
|
59
|
+
'<<=',
|
|
60
|
+
'>>=',
|
|
61
|
+
'>>>',
|
|
62
|
+
'..<',
|
|
63
|
+
'?.',
|
|
64
|
+
'?:',
|
|
65
|
+
'*.',
|
|
66
|
+
'.&',
|
|
67
|
+
'.@',
|
|
68
|
+
'..',
|
|
69
|
+
'::',
|
|
70
|
+
'->',
|
|
71
|
+
'==',
|
|
72
|
+
'!=',
|
|
73
|
+
'<=',
|
|
74
|
+
'>=',
|
|
75
|
+
'&&',
|
|
76
|
+
'||',
|
|
77
|
+
'=~',
|
|
78
|
+
'++',
|
|
79
|
+
'--',
|
|
80
|
+
'+=',
|
|
81
|
+
'-=',
|
|
82
|
+
'*=',
|
|
83
|
+
'%=',
|
|
84
|
+
'&=',
|
|
85
|
+
'|=',
|
|
86
|
+
'^=',
|
|
87
|
+
'**',
|
|
88
|
+
'<<',
|
|
89
|
+
'>>',
|
|
90
|
+
'<>',
|
|
91
|
+
'=',
|
|
92
|
+
'<',
|
|
93
|
+
'>',
|
|
94
|
+
'!',
|
|
95
|
+
'~',
|
|
96
|
+
'+',
|
|
97
|
+
'-',
|
|
98
|
+
'*',
|
|
99
|
+
'%',
|
|
100
|
+
'&',
|
|
101
|
+
'|',
|
|
102
|
+
'^',
|
|
103
|
+
'?',
|
|
104
|
+
':',
|
|
105
|
+
'.',
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Operators that end a postfix chain with a member name.
|
|
109
|
+
const MEMBER_OPERATORS = new Set(['.', '?.', '*.', '.&', '.@', '::']);
|
|
110
|
+
// Operators an operand may start with.
|
|
111
|
+
const PREFIX_OPERATORS = new Set(['!', '~', '+', '-', '++', '--']);
|
|
112
|
+
// Operators that can never start an operand: one of these where an operand
|
|
113
|
+
// must start is the error Groovy reports at that operator (recorded for ==,
|
|
114
|
+
// &&, ||, > and &).
|
|
115
|
+
const NEVER_LEADING = new Set([
|
|
116
|
+
'==',
|
|
117
|
+
'!=',
|
|
118
|
+
'===',
|
|
119
|
+
'!==',
|
|
120
|
+
'<=',
|
|
121
|
+
'>=',
|
|
122
|
+
'<',
|
|
123
|
+
'>',
|
|
124
|
+
'&&',
|
|
125
|
+
'||',
|
|
126
|
+
'=',
|
|
127
|
+
'&',
|
|
128
|
+
'|',
|
|
129
|
+
'^',
|
|
130
|
+
'=~',
|
|
131
|
+
'==~',
|
|
132
|
+
'<=>',
|
|
133
|
+
]);
|
|
134
|
+
// Binary operators; this engine's AND / OR keywords count too, so a criteria
|
|
135
|
+
// in its own dialect gets no syntax verdict here: criteriaDialect.ts gives
|
|
136
|
+
// GSF's.
|
|
137
|
+
const BINARY_KEYWORDS = new Set(['in', 'instanceof', 'as']);
|
|
138
|
+
const DIALECT_BINARY = new Set(['AND', 'OR']);
|
|
139
|
+
|
|
140
|
+
const NAME_START = /[A-Za-z_$]/;
|
|
141
|
+
const NAME_PART = /[A-Za-z0-9_$]/;
|
|
142
|
+
const NUMBER = /^\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[GgLlDdFfIi]?/;
|
|
143
|
+
// Characters Groovy can't start any token with.
|
|
144
|
+
const NEVER_TOKENS = new Set(['#', '`']);
|
|
145
|
+
const TRIPLE_QUOTE = 3;
|
|
146
|
+
|
|
147
|
+
function scan(source: string): Token[] {
|
|
148
|
+
const tokens: Token[] = [];
|
|
149
|
+
let at = 0;
|
|
150
|
+
while (at < source.length) {
|
|
151
|
+
const char = source[at];
|
|
152
|
+
if (char === ' ' || char === '\t') {
|
|
153
|
+
at += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// A line break can end a statement in Groovy: leave those to Groovy.
|
|
157
|
+
if (char === '\n' || char === '\r') throw new GiveUp();
|
|
158
|
+
if (char === "'" || char === '"') {
|
|
159
|
+
// Triple-quoted strings and interpolated GStrings: not modelled.
|
|
160
|
+
if (source.startsWith(char.repeat(TRIPLE_QUOTE), at)) throw new GiveUp();
|
|
161
|
+
let end = at + 1;
|
|
162
|
+
while (end < source.length && source[end] !== char) end += source[end] === '\\' ? 2 : 1;
|
|
163
|
+
if (end >= source.length) throw new Refused(char, at, 'character');
|
|
164
|
+
const text = source.slice(at, end + 1);
|
|
165
|
+
if (char === '"' && text.includes('${')) throw new GiveUp();
|
|
166
|
+
tokens.push({ kind: 'literal', text, offset: at });
|
|
167
|
+
at = end + 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const number = NUMBER.exec(source.slice(at));
|
|
171
|
+
if (number) {
|
|
172
|
+
tokens.push({ kind: 'literal', text: number[0], offset: at });
|
|
173
|
+
at += number[0].length;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// A slash may open a slashy string (/O'Brien/) or a comment, inside
|
|
177
|
+
// which nothing above applies: no verdict.
|
|
178
|
+
if (char === '/') throw new GiveUp();
|
|
179
|
+
if (NAME_START.test(char)) {
|
|
180
|
+
let end = at + 1;
|
|
181
|
+
while (end < source.length && NAME_PART.test(source[end])) end += 1;
|
|
182
|
+
tokens.push({ kind: 'name', text: source.slice(at, end), offset: at });
|
|
183
|
+
at = end;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if ('([{'.includes(char)) {
|
|
187
|
+
tokens.push({ kind: 'open', text: char, offset: at });
|
|
188
|
+
at += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (')]}'.includes(char)) {
|
|
192
|
+
tokens.push({ kind: 'close', text: char, offset: at });
|
|
193
|
+
at += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === ',' || char === ';') {
|
|
197
|
+
tokens.push({ kind: char === ',' ? 'comma' : 'semicolon', text: char, offset: at });
|
|
198
|
+
at += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const operator = OPERATORS.find((candidate) => source.startsWith(candidate, at));
|
|
202
|
+
if (operator) {
|
|
203
|
+
tokens.push({ kind: 'operator', text: operator, offset: at });
|
|
204
|
+
at += operator.length;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (NEVER_TOKENS.has(char)) throw new Refused(char, at, 'character');
|
|
208
|
+
throw new GiveUp();
|
|
209
|
+
}
|
|
210
|
+
return tokens;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class Checker {
|
|
214
|
+
private at = 0;
|
|
215
|
+
private readonly tokens: Token[];
|
|
216
|
+
// The offset of the end of the input, where Groovy reports '<EOF>'.
|
|
217
|
+
private readonly end: number;
|
|
218
|
+
|
|
219
|
+
constructor(tokens: Token[], end: number) {
|
|
220
|
+
this.tokens = tokens;
|
|
221
|
+
this.end = end;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private peek(ahead = 0): Token | undefined {
|
|
225
|
+
return this.tokens[this.at + ahead];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private next(): Token | undefined {
|
|
229
|
+
const token = this.tokens[this.at];
|
|
230
|
+
this.at += 1;
|
|
231
|
+
return token;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private isOperator(token: Token | undefined, ...texts: string[]): boolean {
|
|
235
|
+
return token?.kind === 'operator' && texts.includes(token.text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private startsOperand(token: Token | undefined): boolean {
|
|
239
|
+
if (!token) return false;
|
|
240
|
+
if (token.kind === 'name' || token.kind === 'literal') return true;
|
|
241
|
+
if (token.kind === 'open') return true;
|
|
242
|
+
return token.kind === 'operator' && PREFIX_OPERATORS.has(token.text);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private isBinary(token: Token | undefined): boolean {
|
|
246
|
+
if (!token) return false;
|
|
247
|
+
if (token.kind === 'name') {
|
|
248
|
+
return BINARY_KEYWORDS.has(token.text) || DIALECT_BINARY.has(token.text.toUpperCase());
|
|
249
|
+
}
|
|
250
|
+
return (
|
|
251
|
+
token.kind === 'operator' &&
|
|
252
|
+
!MEMBER_OPERATORS.has(token.text) &&
|
|
253
|
+
!['!', '~', '++', '--', '?', '?:', ':', '->'].includes(token.text)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
check(): void {
|
|
258
|
+
for (;;) {
|
|
259
|
+
this.expression();
|
|
260
|
+
const token = this.next();
|
|
261
|
+
if (!token) return;
|
|
262
|
+
if (token.kind === 'semicolon') {
|
|
263
|
+
if (!this.peek()) return;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// A parenthesis closing nothing (recorded: `NAME == 'x')`).
|
|
267
|
+
if (token.kind === 'close' && token.text === ')') throw new Refused(')', token.offset);
|
|
268
|
+
throw new GiveUp();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private expression(): void {
|
|
273
|
+
this.binary();
|
|
274
|
+
const token = this.peek();
|
|
275
|
+
if (this.isOperator(token, '?')) {
|
|
276
|
+
this.next();
|
|
277
|
+
this.expression();
|
|
278
|
+
const colon = this.next();
|
|
279
|
+
if (!colon) throw new Refused('<EOF>', this.end);
|
|
280
|
+
if (!this.isOperator(colon, ':')) throw new GiveUp();
|
|
281
|
+
this.expression();
|
|
282
|
+
} else if (this.isOperator(token, '?:')) {
|
|
283
|
+
this.next();
|
|
284
|
+
this.expression();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private binary(): void {
|
|
289
|
+
this.unary();
|
|
290
|
+
while (this.isBinary(this.peek())) {
|
|
291
|
+
const operator = this.next()!;
|
|
292
|
+
// A trailing + or - (which could also start an operand) is reported at
|
|
293
|
+
// itself (recorded: `QUANTITY > 1 +`); any other at the end of input.
|
|
294
|
+
if (!this.peek() && PREFIX_OPERATORS.has(operator.text)) {
|
|
295
|
+
throw new Refused(operator.text, operator.offset);
|
|
296
|
+
}
|
|
297
|
+
this.unary();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private unary(): void {
|
|
302
|
+
for (;;) {
|
|
303
|
+
const token = this.peek();
|
|
304
|
+
if (token?.kind === 'operator' && PREFIX_OPERATORS.has(token.text)) {
|
|
305
|
+
this.next();
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// This engine's NOT keyword, when an operand follows it.
|
|
309
|
+
if (
|
|
310
|
+
token?.kind === 'name' &&
|
|
311
|
+
token.text.toUpperCase() === 'NOT' &&
|
|
312
|
+
this.startsOperand(this.peek(1))
|
|
313
|
+
) {
|
|
314
|
+
this.next();
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
this.postfix();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private postfix(): void {
|
|
323
|
+
this.primary();
|
|
324
|
+
for (;;) {
|
|
325
|
+
const token = this.peek();
|
|
326
|
+
if (token?.kind === 'operator' && MEMBER_OPERATORS.has(token.text)) {
|
|
327
|
+
this.next();
|
|
328
|
+
const member = this.next();
|
|
329
|
+
if (!member) throw new Refused('<EOF>', this.end);
|
|
330
|
+
// A name, or a quoted one (obj.'name'); not a number (1.5.5).
|
|
331
|
+
const quoted = member.kind === 'literal' && /^['"]/.test(member.text);
|
|
332
|
+
if (member.kind !== 'name' && !quoted) throw new GiveUp();
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (this.isOperator(token, '++', '--')) {
|
|
336
|
+
this.next();
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (token?.kind === 'open' && token.text === '(') {
|
|
340
|
+
this.next();
|
|
341
|
+
this.callArguments(token);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (token?.kind === 'open' && (token.text === '[' || token.text === '{')) {
|
|
345
|
+
this.next();
|
|
346
|
+
this.bracketed(token);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private primary(): void {
|
|
354
|
+
const token = this.next();
|
|
355
|
+
if (!token) throw new Refused('<EOF>', this.end);
|
|
356
|
+
if (token.kind === 'name' || token.kind === 'literal') return;
|
|
357
|
+
if (token.kind === 'open' && token.text === '(') {
|
|
358
|
+
const following = this.peek();
|
|
359
|
+
if (!following) throw new Refused('(', this.end); // recorded: `NAME == 'x' && (`
|
|
360
|
+
// `()` is no expression; recorded only at the end of the input.
|
|
361
|
+
if (following.kind === 'close' && following.text === ')') {
|
|
362
|
+
this.next();
|
|
363
|
+
if (!this.peek()) throw new Refused('<EOF>', this.end);
|
|
364
|
+
throw new GiveUp();
|
|
365
|
+
}
|
|
366
|
+
this.expression();
|
|
367
|
+
const close = this.next();
|
|
368
|
+
if (!close) throw new Refused('<EOF>', this.end);
|
|
369
|
+
if (close.kind !== 'close' || close.text !== ')') throw new GiveUp();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (token.kind === 'open') {
|
|
373
|
+
this.bracketed(token);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (token.kind === 'close' && token.text === ')') throw new Refused(')', token.offset);
|
|
377
|
+
if (token.kind === 'operator' && NEVER_LEADING.has(token.text)) {
|
|
378
|
+
throw new Refused(token.text, token.offset);
|
|
379
|
+
}
|
|
380
|
+
throw new GiveUp();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// A call's arguments, after its '('. Running out of input inside them is
|
|
384
|
+
// reported at that '(' (recorded: `Expr.contains(NAME, 'x'`).
|
|
385
|
+
private callArguments(open: Token): void {
|
|
386
|
+
try {
|
|
387
|
+
this.list(')');
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (error instanceof Refused && error.offset === this.end && error.kind === 'input') {
|
|
390
|
+
throw new Refused('(', open.offset);
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// A list literal or index after '[', or a closure after '{'. Closures
|
|
397
|
+
// aren't modelled: their contents only have to balance.
|
|
398
|
+
private bracketed(open: Token): void {
|
|
399
|
+
if (open.text === '{') {
|
|
400
|
+
let depth = 1;
|
|
401
|
+
while (depth > 0) {
|
|
402
|
+
const token = this.next();
|
|
403
|
+
if (!token) throw new GiveUp();
|
|
404
|
+
if (token.kind === 'open') depth += 1;
|
|
405
|
+
if (token.kind === 'close') depth -= 1;
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.list(']');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Comma-separated expressions up to `close`; a trailing comma is allowed.
|
|
413
|
+
private list(close: string): void {
|
|
414
|
+
for (;;) {
|
|
415
|
+
const token = this.peek();
|
|
416
|
+
if (token?.kind === 'close' && token.text === close) {
|
|
417
|
+
this.next();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (token?.kind === 'comma') throw new GiveUp();
|
|
421
|
+
this.expression();
|
|
422
|
+
const after = this.next();
|
|
423
|
+
if (!after) {
|
|
424
|
+
if (close === ')') throw new Refused('<EOF>', this.end);
|
|
425
|
+
throw new GiveUp();
|
|
426
|
+
}
|
|
427
|
+
if (after.kind === 'close' && after.text === close) return;
|
|
428
|
+
if (after.kind !== 'comma') throw new GiveUp();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// The criteria's Groovy tokens, or undefined where this scanner gives up (a
|
|
434
|
+
// line break, a slash, a triple-quoted or interpolated string) or Groovy's
|
|
435
|
+
// lexer refuses it.
|
|
436
|
+
export function groovyTokens(criteria: string): Token[] | undefined {
|
|
437
|
+
try {
|
|
438
|
+
return scan(criteria);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof GiveUp || error instanceof Refused) return undefined;
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// The syntax error Groovy would report for this criteria, or undefined when
|
|
446
|
+
// it has none or this check can't tell (see the header).
|
|
447
|
+
export function groovySyntaxError(criteria: string): GroovySyntaxError | undefined {
|
|
448
|
+
const verdict = checkGroovySyntax(criteria);
|
|
449
|
+
return verdict.kind === 'refused' ? verdict.error : undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// The whole verdict: 'valid' when the criteria parsed as Groovy this check
|
|
453
|
+
// understands, 'refused' with Groovy's error, 'unknown' when it gave up (a
|
|
454
|
+
// construct it doesn't model, or nesting too deep for its recursion).
|
|
455
|
+
export type GroovyVerdict =
|
|
456
|
+
| { kind: 'valid' }
|
|
457
|
+
| { kind: 'refused'; error: GroovySyntaxError }
|
|
458
|
+
| { kind: 'unknown' };
|
|
459
|
+
|
|
460
|
+
export function checkGroovySyntax(criteria: string): GroovyVerdict {
|
|
461
|
+
if (criteria.trim() === '') return { kind: 'valid' };
|
|
462
|
+
try {
|
|
463
|
+
new Checker(scan(criteria), criteria.length).check();
|
|
464
|
+
return { kind: 'valid' };
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (error instanceof GiveUp || error instanceof RangeError) return { kind: 'unknown' };
|
|
467
|
+
if (!(error instanceof Refused)) throw error;
|
|
468
|
+
const quoted = error.text === "'" ? "'\\''" : `'${error.text}'`;
|
|
469
|
+
return {
|
|
470
|
+
kind: 'refused',
|
|
471
|
+
error: { message: `Unexpected ${error.kind}: ${quoted}`, line: 1, column: error.offset + 1 },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let scripts = 0;
|
|
477
|
+
|
|
478
|
+
// The INVALID_CRITERIA TEXT GSF sends for it: the MultipleCompilationErrors
|
|
479
|
+
// message, prefixed with the exception's simple name
|
|
480
|
+
// (GenericCriteriaValidationException("${e.javaClass.simpleName} :
|
|
481
|
+
// ${e.message}")), naming a generated script class.
|
|
482
|
+
// `ownLine`: the position goes on a line of its own, as Groovy prints a
|
|
483
|
+
// semantic error ("unable to resolve class NOT\n @ line 1, column 5.")
|
|
484
|
+
// rather than a syntax one.
|
|
485
|
+
export function groovyCompileErrorText(
|
|
486
|
+
criteria: string,
|
|
487
|
+
error: GroovySyntaxError,
|
|
488
|
+
{ ownLine = false }: { ownLine?: boolean } = {},
|
|
489
|
+
): string {
|
|
490
|
+
scripts += 1;
|
|
491
|
+
const script = `Script${Date.now()}${scripts}.groovy`;
|
|
492
|
+
const sourceLine = criteria.split('\n')[error.line - 1] ?? '';
|
|
493
|
+
return (
|
|
494
|
+
'MultipleCompilationErrorsException : startup failed:\n' +
|
|
495
|
+
`${script}: ${error.line}: ${error.message}${ownLine ? '\n' : ''} @ line ${error.line}, column ${error.column}.\n` +
|
|
496
|
+
` ${sourceLine}\n` +
|
|
497
|
+
` ${' '.repeat(error.column - 1)}^\n` +
|
|
498
|
+
'\n1 error\n'
|
|
499
|
+
);
|
|
500
|
+
}
|