@sap/cds-compiler 5.1.0 → 5.2.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/cdsc.js +2 -2
  3. package/bin/cdshi.js +24 -17
  4. package/bin/cdsse.js +17 -18
  5. package/lib/api/main.js +19 -2
  6. package/lib/api/options.js +4 -1
  7. package/lib/base/builtins.js +1 -0
  8. package/lib/base/message-registry.js +16 -3
  9. package/lib/base/model.js +0 -10
  10. package/lib/checks/actionsFunctions.js +0 -12
  11. package/lib/checks/structuredAnnoExpressions.js +10 -14
  12. package/lib/compiler/assert-consistency.js +19 -11
  13. package/lib/compiler/builtins.js +1 -1
  14. package/lib/compiler/define.js +6 -4
  15. package/lib/compiler/extend.js +5 -5
  16. package/lib/compiler/populate.js +9 -9
  17. package/lib/compiler/propagator.js +1 -0
  18. package/lib/compiler/resolve.js +29 -34
  19. package/lib/compiler/shared.js +7 -8
  20. package/lib/compiler/tweak-assocs.js +155 -64
  21. package/lib/compiler/utils.js +1 -1
  22. package/lib/compiler/xpr-rewrite.js +4 -3
  23. package/lib/edm/annotations/genericTranslation.js +13 -9
  24. package/lib/edm/csn2edm.js +26 -2
  25. package/lib/edm/edm.js +23 -8
  26. package/lib/edm/edmInboundChecks.js +5 -7
  27. package/lib/edm/edmPreprocessor.js +43 -30
  28. package/lib/gen/BaseParser.js +720 -0
  29. package/lib/gen/CdlParser.js +4421 -0
  30. package/lib/gen/language.checksum +1 -1
  31. package/lib/gen/language.interp +1 -1
  32. package/lib/gen/languageParser.js +4006 -4001
  33. package/lib/language/antlrParser.js +62 -0
  34. package/lib/language/genericAntlrParser.js +28 -0
  35. package/lib/model/csnUtils.js +2 -0
  36. package/lib/model/revealInternalProperties.js +2 -0
  37. package/lib/modelCompare/utils/filter.js +70 -42
  38. package/lib/optionProcessor.js +9 -3
  39. package/lib/parsers/AstBuildingParser.js +1172 -0
  40. package/lib/parsers/CdlGrammar.g4 +1940 -0
  41. package/lib/parsers/Lexer.js +239 -0
  42. package/lib/render/toCdl.js +23 -27
  43. package/lib/render/toSql.js +5 -5
  44. package/lib/transform/db/applyTransformations.js +54 -16
  45. package/lib/transform/draft/odata.js +10 -11
  46. package/lib/transform/effective/flattening.js +10 -14
  47. package/lib/transform/odata/flattening.js +42 -31
  48. package/lib/transform/odata/toFinalBaseType.js +7 -6
  49. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  50. package/package.json +2 -2
  51. package/share/messages/redirected-to-ambiguous.md +5 -4
@@ -0,0 +1,1172 @@
1
+ 'use strict';
2
+
3
+ const BaseParser = require( '../gen/BaseParser' );
4
+
5
+ const { Location } = require( '../base/location' );
6
+ const { dictAdd, dictAddArray } = require('../base/dictionaries');
7
+ const { functionsWithoutParens } = require('../base/builtins');
8
+
9
+ const { pathName } = require('../compiler/utils');
10
+ const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
11
+ const parserTokens = { // TODO: precompile into specialFunction
12
+ GenericIntro: 'intro',
13
+ GenericExpr: 'expr',
14
+ GenericSeparator: 'separator',
15
+ };
16
+
17
+ const { parseMultiLineStringLiteral } = require('../language/multiLineStringParser');
18
+ const { normalizeNewLine, normalizeNumberString } = require('../language/textUtils');
19
+ const { parseDocComment } = require('../language/docCommentParser');
20
+
21
+ const $location = Symbol.for('cds.$location');
22
+
23
+ const extensionDicts = {
24
+ elements: true, enum: true, params: true, returns: true,
25
+ };
26
+
27
+ const keywordTypeNames = {
28
+ association: 'cds.Association', composition: 'cds.Composition',
29
+ };
30
+
31
+ const queryOps = {
32
+ SELECT: 'query',
33
+ union: 'query',
34
+ intersect: 'query',
35
+ except: 'query',
36
+ minus: 'query',
37
+ };
38
+
39
+ const PRECEDENCE_OF_IN_PREDICATE = 10;
40
+
41
+ class AstBuildingParser extends BaseParser {
42
+ constructor( lexer, keywords, table, options, messageFunctions ) {
43
+ super( lexer, keywords, table ); // lexer has file
44
+ this.options = options;
45
+ this.$messageFunctions = messageFunctions;
46
+ this.docComments = [];
47
+ this.docCommentIndex = 0;
48
+
49
+ this.afterBrace$ = -1;
50
+ this.topLevel$ = -1;
51
+ }
52
+
53
+ // messages, conditions and other parsing-specific things ---------------------
54
+ error( id, location, args = {}, text = null ) {
55
+ // eslint-disable-next-line cds-compiler/message-call-format
56
+ return this.$messageFunctions.error( id, location?.location || location, args, text );
57
+ }
58
+ message( id, location, args = {}, text = null ) {
59
+ // eslint-disable-next-line cds-compiler/message-call-format
60
+ return this.$messageFunctions.message( id, location?.location || location, args, text );
61
+ }
62
+ warning( id, location, args = {}, text = null ) {
63
+ // eslint-disable-next-line cds-compiler/message-call-format
64
+ return this.$messageFunctions.warning( id, location?.location || location, args, text );
65
+ }
66
+ info( id, location, args = {}, text = null ) {
67
+ // eslint-disable-next-line cds-compiler/message-call-format
68
+ return this.$messageFunctions.info( id, location?.location || location, args, text );
69
+ }
70
+
71
+ expectingArray() {
72
+ const expecting = this._expecting();
73
+ let array = Object.keys( expecting );
74
+ // compatibility: replace true+false by Boolean
75
+ if (expecting.true && expecting.false)
76
+ array = [ 'Boolean', ...array.filter( n => n !== 'true' && n !== 'false' ) ];
77
+ return array.map( antlrName )
78
+ .sort( (a, b) => (tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1) );
79
+ }
80
+
81
+ reportUnexpectedToken_( token ) {
82
+ const expecting = this.expectingArray();
83
+ const err = this.error( 'syntax-unexpected-token', token,
84
+ { offending: antlrName( token ), expecting } );
85
+ // No 'unwanted' variant, no 'syntax-missing-token'
86
+ err.expectedTokens = expecting;
87
+ }
88
+ reportReservedWord_( token ) {
89
+ const err = this.message( 'syntax-unexpected-reserved-word', token,
90
+ { code: token.text, delimited: token.text } );
91
+ // TODO: at least if one expected keyword is similar, mention expected set
92
+ err.expectedTokens = this.expectingArray();
93
+ }
94
+
95
+ setPrecInCallingRule() {
96
+ const caller = this.stack.at( -1 );
97
+ if (this.inSameRule_( caller.ruleState, caller.followState ))
98
+ caller.prec = this.prec_;
99
+ }
100
+
101
+ tableAlias() {
102
+ const { keyword } = this.la();
103
+ if (keyword && this.keywords[keyword])
104
+ return false;
105
+ if (this.lb().type !== ')')
106
+ return true;
107
+ // after ')' we need to check the expression category = must not already be a
108
+ // table, like a simplified version of `<prec=-2, postfix=once>` which we
109
+ // cannot do additionally
110
+ if (this.prec_ != null && this.prec_ <= -2)
111
+ return false;
112
+ this.prec_ = -2;
113
+ return true;
114
+ }
115
+
116
+ prepareSpecialFunction() {
117
+ const func = this.tokens[this.tokenIdx - 2].keyword?.toUpperCase();
118
+ // TODO: use lower-case in specialFunctions
119
+ const spec = specialFunctions[func];
120
+ this.dynamic_.call = { func, argPos: 0 };
121
+ this.dynamic_.generic = spec ? spec[0] : specialFunctions[''][0];
122
+ }
123
+
124
+ nextFunctionArgument() {
125
+ const { call } = this.dynamic_;
126
+ const spec = specialFunctions[call.func];
127
+ ++call.argPos;
128
+ this.dynamic_.generic = spec ? spec[call.argPos] : specialFunctions[''][1];
129
+ }
130
+
131
+ lGenericIntroOrExpr( tryGenericIntro = true ) {
132
+ const { keyword, type } = this.la();
133
+ // TODO: use lower-case in specialFunctions
134
+ const text = keyword?.toUpperCase() ?? type;
135
+ const generic = this.dynamic_.generic?.[text];
136
+ if (tryGenericIntro) {
137
+ if (this.dynamic_.generic?.IN === 'separator')
138
+ this.prec_ = PRECEDENCE_OF_IN_PREDICATE; // only expressions if `in` is separator
139
+ if (generic !== 'expr')
140
+ return (generic === 'intro') ? 'GenericIntro' : 'Id';
141
+ const next = this.tokens[this.tokenIdx + 1];
142
+ if (next.type !== ',' && next.type !== ')' &&
143
+ this.dynamic_.generic[next.keyword?.toUpperCase()] !== 'separator')
144
+ return 'GenericIntro';
145
+ }
146
+ return (generic === 'expr') ? 'GenericExpr' : 'Id';
147
+ }
148
+
149
+ lGenericExpr() {
150
+ return this.lGenericIntroOrExpr( false );
151
+ }
152
+
153
+ lGenericSeparator() {
154
+ const { keyword, type } = this.la();
155
+ // TODO: use lower-case in specialFunctions
156
+ const text = keyword?.toUpperCase() ?? type;
157
+ const generic = this.dynamic_.generic?.[text];
158
+ return (generic === 'separator') ? 'GenericSeparator' : ',';
159
+ }
160
+
161
+ translateParserToken_( tokenName ) {
162
+ const realTokens = this.dynamic_.generic?.[parserTokens[tokenName]];
163
+ // TODO: avoid parserTokens dict, use lower-case in specialFunctions
164
+ return realTokens?.map( s => s.toLowerCase() ) ?? [];
165
+ }
166
+
167
+ isDotForPath() {
168
+ const next = this.tokens[this.tokenIdx + 1].type;
169
+ return next !== '*' && next !== '{';
170
+ }
171
+
172
+ // <prec=10, postfix=once> + test that the next token is not `null`
173
+ // TODO TOOL: allow to provide argument with condition: <cond=isNegatedRelation, arg=10>
174
+ isNegatedRelation() {
175
+ const parentPrec = this.stack.at( -1 ).prec;
176
+ if (parentPrec != null && parentPrec >= 10 ||
177
+ this.prec_ != null && this.prec_ <= 10 ||
178
+ this.tokens[this.tokenIdx + 1].keyword === 'null')
179
+ return false;
180
+ this.prec_ = 10;
181
+ return true;
182
+ }
183
+
184
+ isNamedArg() {
185
+ const { type } = this.tokens[this.tokenIdx + 1];
186
+ return type === ':' || type === '=>';
187
+ }
188
+
189
+ /**
190
+ * `namespace` is forbidden after a definitions/extend or after previous
191
+ * `namespace`
192
+ */
193
+ fileSection() {
194
+ return ++this.topLevel$ < 1;
195
+ }
196
+
197
+ /**
198
+ * `;` between statements is optional only after a `}` (ex braces of structure
199
+ * values for annotations).
200
+ */
201
+ afterBrace( test ) {
202
+ if (!test)
203
+ this.afterBrace$ = this.tokenIdx;
204
+ return this.afterBrace$ === this.tokenIdx;
205
+ }
206
+
207
+ /**
208
+ * Annotation assignments at the end of (element) refs are allowed.
209
+ */
210
+ allowFinalAnnoAssign() {
211
+ // TODO: do properly with type expression
212
+ return this.afterBrace$ !== this.tokenIdx;
213
+ }
214
+
215
+ // TOOL Runtime TODO: provide proto-linked dynamicContext
216
+ inSameLine() {
217
+ return this.lb().location.line === this.la().location.line;
218
+ }
219
+
220
+ /**
221
+ * `...` can appear in the top-level array value only.
222
+ */
223
+ annoTopValue( test ) {
224
+ if (test)
225
+ return !this.stack.at( -1 ).$annoTopValue;
226
+ if (this.stack.at( -2 )?.$annoTopValue)
227
+ this.stack.at( -1 ).$annoTopValue = 'inner';
228
+ else if (this.lb().type === '[')
229
+ this.stack.at( -1 ).$annoTopValue = 'array';
230
+ return null;
231
+ }
232
+
233
+ beforeColon() {
234
+ return this.tokens[this.tokenIdx + 1]?.text === ':';
235
+ }
236
+
237
+ // Space handling etc, locations ----------------------------------------------
238
+
239
+ // Use the following method for language constructs which we (currently) do
240
+ // not really compile, just use to produce a CSN for functions parse.cql() and
241
+ // parse.expr().
242
+ // This function has a similar interface to our message functions on purpose!
243
+ // (tokenAhead ~= location)
244
+ csnParseOnly( msgId, tokenAhead, textArgs ) {
245
+ if (this.options.parseOnly)
246
+ return;
247
+ // assumes no value < -1:
248
+ const location = (tokenAhead > 0)
249
+ ? this.combineLocation( this.la(), this.tokens[this.tokenIdx + tokenAhead] )
250
+ : this.tokens[this.tokenIdx + tokenAhead].location;
251
+ this.error( msgId, location, textArgs );
252
+ }
253
+
254
+ warnIfColonFollows( anno ) {
255
+ if (this.l() === ':') {
256
+ this.warning( 'syntax-missing-parens', anno.name,
257
+ { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
258
+ // eslint-disable-next-line max-len
259
+ 'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
260
+ }
261
+ }
262
+
263
+ noAssignmentInSameLine() {
264
+ const next = this.la();
265
+ if (next.text === '@' && next.line <= this.lb().endLine) {
266
+ this.warning( 'syntax-missing-semicolon', next, { code: ';' },
267
+ // eslint-disable-next-line max-len
268
+ 'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' );
269
+ }
270
+ }
271
+
272
+ // For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths
273
+ reportUnexpectedSpace( prefix = this.lb(),
274
+ location = this.la().location,
275
+ isError = false ) {
276
+ const prefixLoc = prefix.location;
277
+ if (prefixLoc.endLine !== location.line ||
278
+ prefixLoc.endCol !== location.col) {
279
+ const wsLocation = {
280
+ __proto__: Location.prototype,
281
+ file: location.file,
282
+ line: prefixLoc.endLine, // !
283
+ col: prefixLoc.endCol, // !
284
+ endLine: location.line,
285
+ endCol: location.col,
286
+ };
287
+ if (isError) {
288
+ this.message( 'syntax-invalid-space', wsLocation, { op: prefix.text },
289
+ 'Delete the whitespace after $(OP)' );
290
+ }
291
+ else {
292
+ this.warning( 'syntax-unexpected-space', wsLocation, { op: prefix.text },
293
+ 'Delete the whitespace after $(OP)' );
294
+ }
295
+ }
296
+ return prefixLoc;
297
+ }
298
+
299
+ startLocation( { location } = this.lr() ) {
300
+ return {
301
+ __proto__: Location.prototype,
302
+ file: location.file,
303
+ line: location.line,
304
+ col: location.col,
305
+ endLine: undefined,
306
+ endCol: undefined,
307
+ };
308
+ }
309
+
310
+ attachLocation( art ) {
311
+ if (!art)
312
+ return art;
313
+ art.location ??= this.startLocation();
314
+ if (this.s == null) // do not set end location if error
315
+ return art;
316
+ const { location } = this.lb();
317
+ art.location.endLine = location.endLine;
318
+ art.location.endCol = location.endCol;
319
+ return art;
320
+ }
321
+
322
+ ruleTokensText() {
323
+ let tokenIdx = this.stack.at(-1).tokenIdx + 1;
324
+ const stop = this.tokenIdx - 1;
325
+
326
+ let { text: result, location: prev } = this.tokens[tokenIdx];
327
+ while (++tokenIdx < stop) {
328
+ const { text, location } = this.tokens[tokenIdx];
329
+ if (location.line > prev.endLine ||
330
+ location.line === prev.endLine && location.col > prev.endCol)
331
+ result += ' ';
332
+ result += normalizeNewLine( text );
333
+ prev = location;
334
+ }
335
+ return result;
336
+ }
337
+
338
+ // AST building ---------------------------------------------------------------
339
+
340
+ assignAnnotation( art, val, name, prefix = '' ) {
341
+ const { path } = name;
342
+ const pathname = pathName( path );
343
+ if (!pathname)
344
+ return;
345
+ let absolute = '';
346
+ if (name.variant) {
347
+ const variant = pathName( name.variant.path );
348
+ absolute = `${ prefix }${ pathname }#${ variant }`;
349
+ // We do not care anymore whether we get a second '#' with flattening. This
350
+ // can be produced via CSN and with delimited ids anyway. If backends care,
351
+ // they need to have their own check.
352
+ }
353
+ else if (!prefix || pathname !== '$value') {
354
+ absolute = `${ prefix }${ pathname }`;
355
+ }
356
+ else {
357
+ absolute = prefix.slice( 0, -1 ); // remove final dot
358
+ }
359
+
360
+ val.name = name;
361
+ if (val.$flatten) {
362
+ for (const a of val.$flatten)
363
+ this.assignAnnotation( art, a, a.name, `${ absolute }.` );
364
+ }
365
+ else {
366
+ name.id = absolute;
367
+ this.addAnnotation( art, `@${ absolute }`, val );
368
+ }
369
+ if (!prefix) { // set deprecated $annotations for cds-lsp
370
+ const { line, col } = name.location;
371
+ const location = {
372
+ __proto__: Location.prototype,
373
+ ...val.location,
374
+ line,
375
+ col,
376
+ };
377
+ art.$annotations ??= [];
378
+ art.$annotations.push( { value: val, location } );
379
+ }
380
+ }
381
+
382
+ addAnnotation( art, prop, anno ) {
383
+ const old = art[prop];
384
+ if (old) {
385
+ this.error( 'syntax-duplicate-anno', old.name, { anno: prop },
386
+ 'Assignment for $(ANNO) is overwritten by another one below' );
387
+ }
388
+ art[prop] = anno;
389
+ }
390
+
391
+ identAst( token = this.lb() ) {
392
+ const { text, keyword, location } = token;
393
+ if (keyword) // no delimited id, see Lexer.js
394
+ return { id: text, location };
395
+ const close = keyword === 0 ? Infinity : -1;
396
+ const id = (text.charAt(0) === '!')
397
+ ? text.slice( 2, close ).replace( /]]/g, ']' )
398
+ : text.slice( 1, close ).replace( /""/g, '"' );
399
+
400
+ if (keyword !== 0) {
401
+ if (!id) {
402
+ this.message( 'syntax-invalid-name', location, {} );
403
+ }
404
+ else if (text.charAt(0) !== '!') {
405
+ this.message( 'syntax-deprecated-ident', location, { delimited: id },
406
+ // eslint-disable-next-line max-len
407
+ 'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' );
408
+ }
409
+ }
410
+ // $delimited is used to complain about ![$self] and other magic vars usage;
411
+ // we might complain about that already here via @arg{category}
412
+ return { id, location, $delimited: true };
413
+ }
414
+
415
+ fragileAlias( safe = false ) {
416
+ const ast = this.identAst();
417
+ if (safe || ast.$delimited || !/^[a-zA-Z][a-zA-Z_]+$/.test( ast.id )) {
418
+ this.warning( 'syntax-deprecated-auto-as', ast.location, { keyword: 'as' },
419
+ 'Add keyword $(KEYWORD) in front of the alias name' );
420
+ }
421
+ else { // configurable error
422
+ this.message( 'syntax-missing-as', ast.location, { keyword: 'as' },
423
+ 'Add keyword $(KEYWORD) in front of the alias name' );
424
+ }
425
+ return ast;
426
+ }
427
+
428
+ identAstWithPrefix( prefix, token = this.lb() ) {
429
+ const ast = this.identAst( token );
430
+ const { line, col } = prefix.location;
431
+ // TODO main: location method `withEndLocation`
432
+ ast.location = {
433
+ __proto__: Location.prototype,
434
+ ...token.location,
435
+ line,
436
+ col,
437
+ };
438
+ ast.id = prefix.text + ast.id;
439
+ return ast;
440
+ }
441
+
442
+ classifyImplicitName( category, ref ) {
443
+ if (!ref || ref.path) {
444
+ const tokenIndex = ref?.path[ref.path.length - 1]?.location.tokenIndex;
445
+ const token = this.tokens[tokenIndex ?? this.tokenIdx - 1];
446
+ const { parsed } = token;
447
+ if (parsed && parsed !== 'token' && parsed !== 'keyword') {
448
+ token.parsed = category;
449
+ return { token, parsed };
450
+ }
451
+ }
452
+ return null;
453
+ }
454
+
455
+ taggedIfQuery( query ) {
456
+ return (query.op && queryOps[query.op.val])
457
+ ? { query, location: query.$parens?.at( -1 ) ?? query.location }
458
+ : query;
459
+ }
460
+
461
+ addNamedArg( args, idToken, expr ) {
462
+ expr.name = this.identAst( idToken );
463
+ (args.args ?? args)[expr.name.id] = expr;
464
+ }
465
+
466
+ ixprAst( args ) {
467
+ if (args.length === 1)
468
+ return args[0];
469
+ return this.attachLocation( { op: { val: 'ixpr', location: this.lr().location }, args } );
470
+ }
471
+
472
+ // Create AST node for quoted literals like string and e.g. date'2017-02-22'.
473
+ // This function might issue a message and might change the `literal` and
474
+ // `val` property according to `quotedLiteralPatterns` above.
475
+ quotedLiteral( token = this.lb() ) {
476
+ const { location, text } = token;
477
+ let literal = 'string';
478
+ let pos;
479
+ let val;
480
+
481
+ if (text.startsWith( '`' )) {
482
+ val = parseMultiLineStringLiteral.call( this, token ); // TODO: remove `call()` syntax
483
+ }
484
+ else {
485
+ pos = text.search( '\'' ) + 1; // pos of char after quote
486
+ val = text.slice( pos, -1 ).replace( /''/g, '\'' );
487
+ }
488
+
489
+ if (pos > 1)
490
+ literal = text.slice( 0, pos - 1 ).toLowerCase();
491
+ const p = quotedLiteralPatterns[literal] || {};
492
+
493
+ if (p.test_fn && !p.test_fn( val ) && !this.options.parseOnly)
494
+ this.warning( 'syntax-invalid-literal', location, { '#': p.test_variant } );
495
+
496
+ if (p.unexpected_char) {
497
+ const idx = val.search( p.unexpected_char );
498
+ if (idx > -1) {
499
+ this.warning( 'syntax-invalid-literal', {
500
+ file: location.file,
501
+ line: location.line,
502
+ endLine: location.line,
503
+ col: atChar(idx),
504
+ endCol: atChar( idx + (val[idx] === '\'' ? 2 : 1) ),
505
+ }, { '#': p.unexpected_variant } );
506
+ }
507
+ }
508
+ return { literal, val: p.normalize?.(val) || val, location };
509
+
510
+ function atChar( i ) {
511
+ // Is only used with single-line strings.
512
+ return location.col + pos + i;
513
+ }
514
+ }
515
+
516
+ // If a '-' is directly before an unsigned number, consider it part of the number;
517
+ // otherwise (including for '+'), represent it as extra unary prefix operator.
518
+ signedExpression( ixpr, expr ) {
519
+ // if (args.length !== 1) throw new CompilerAssertion()
520
+ const sign = ixpr.args[0];
521
+ const nval
522
+ = (sign.val === '-' &&
523
+ expr && // expr may be null if `-` rule can't be parsed
524
+ expr.literal === 'number' &&
525
+ sign.location.endLine === expr.location.line &&
526
+ sign.location.endCol === expr.location.col &&
527
+ ( typeof expr.val === 'number'
528
+ ? expr.val >= 0 && -expr.val
529
+ : !expr.val.startsWith('-') && `-${ expr.val }`)) || false;
530
+ if (nval === false) {
531
+ ixpr.args.push( expr );
532
+ return this.attachLocation( ixpr );
533
+ }
534
+ expr.val = nval;
535
+ --expr.location.col;
536
+ return expr;
537
+ }
538
+
539
+ /**
540
+ * Given `token`, return a number literal (XSN). If the number is not an unsigned integer
541
+ * or it can't be represented in JS, emit an error.
542
+ */
543
+ unsignedIntegerLiteral() {
544
+ const token = this.lb();
545
+ const { location } = token;
546
+ const text = token.text || '0';
547
+ const num = Number.parseFloat( text ); // not Number.parseInt() !
548
+ if (!Number.isSafeInteger(num)) {
549
+ this.error( 'syntax-expecting-unsigned-int', token,
550
+ { '#': !text.match(/^\d*$/) ? 'normal' : 'unsafe' } );
551
+ }
552
+ else if (text.match(/^\d+[.]\d+$/)) {
553
+ // More restrictive check: 10.0 emits a message, because we don't expect
554
+ // any decimal places.
555
+ const dotLoc = { ...location };
556
+ dotLoc.col += text.indexOf('.');
557
+ dotLoc.endCol = dotLoc.col + 1;
558
+ this.info( 'syntax-ignoring-decimal', dotLoc );
559
+ }
560
+ return { literal: 'number', val: num, location };
561
+ }
562
+
563
+ numberLiteral( sign = null ) {
564
+ const token = this.lb();
565
+ let { location } = token;
566
+ const { keyword, location: nextLoc } = this.la();
567
+ if (keyword && // is only set with keyword and/or non-delimited Id
568
+ nextLoc.line === location.endLine && nextLoc.col === location.endCol) {
569
+ this.message( 'syntax-expecting-space', nextLoc, {},
570
+ 'Expecting a space between a number and a keyword/identifier' );
571
+ }
572
+
573
+ const text = (sign) ? sign.text + token.text : token.text;
574
+ if (sign) {
575
+ this.reportUnexpectedSpace( sign, location );
576
+ location = {
577
+ __proto__: Location.prototype,
578
+ ...sign.location,
579
+ endLine: location.endLine,
580
+ endCol: location.endCol,
581
+ };
582
+ }
583
+ const val = Number.parseFloat( text || '0' ); // not Number.parseInt() !
584
+ const normalized = normalizeNumberString( text );
585
+ if (normalized === `${ val }` || sign && normalized === `${ sign.text }${ val }`)
586
+ return { literal: 'number', val, location };
587
+ return { literal: 'number', val: normalized, location };
588
+ }
589
+
590
+ adjustAnnoNumber( value ) {
591
+ const { val } = value;
592
+ if (value.literal !== 'number' || typeof val === 'number')
593
+ return;
594
+ // a number in CDL, but stored as string in `val` - due to rounding or scientific notation
595
+ const num = Number.parseFloat( val || '0' );
596
+ const infinite = !Number.isFinite( num );
597
+ if (infinite || relevantDigits( val ) !== relevantDigits( num.toString() )) {
598
+ this.warning( 'syntax-invalid-anno-number', value,
599
+ { '#': (infinite ? 'infinite' : 'rounded' ), rawvalue: val, value: num },
600
+ {
601
+ std: 'Annotation number $(RAWVALUE) is put as $(VALUE) into the CSN',
602
+ rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)',
603
+ // eslint-disable-next-line max-len
604
+ infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN',
605
+ } );
606
+ }
607
+ if (!infinite)
608
+ value.val = num;
609
+ }
610
+
611
+ /**
612
+ * Store doc comment between previous and current token as `art.doc`. If `art`
613
+ * is not provided (with EOF), just complain about remaining doc comment tokens.
614
+ *
615
+ * The doc comment token is not a “standard” token for the following reasons:
616
+ * - misplaced doc comments would lead to a parse error (incompatible),
617
+ * - would influence the prediction and error recovery,
618
+ * - is only slightly "more declarative" in the grammar.
619
+ */
620
+ docComment( art ) {
621
+ const { line: prevLine, col: prevCol } = this.lb()?.location ?? { line: 0, col: 0 };
622
+ const { line: currLine, col: currCol } = this.la().location;
623
+ let token;
624
+ for (;;) {
625
+ token = this.docComments[this.docCommentIndex];
626
+ if (!token)
627
+ return; // no further doc comment
628
+ const { line, col } = token.location;
629
+ if (art && (line > currLine || line === currLine && col > currCol))
630
+ return; // next doc comment after current token
631
+
632
+ ++this.docCommentIndex;
633
+ if (!art || line < prevLine || line === prevLine && col < prevCol) {
634
+ if (this.options.docComment !== false) {
635
+ this.info( 'syntax-ignoring-doc-comment', token.location, {},
636
+ 'Ignoring doc comment as it is not written at a defined position' );
637
+ }
638
+ }
639
+ else { // next doc comment between previous & current token
640
+ // With explicit docComment:false, we don't emit a warning.
641
+ if (art.doc && this.options.docComment !== false) {
642
+ this.docComments[art.doc.location.tokenIndex].parsed = '';
643
+ this.warning( 'syntax-duplicate-doc-comment', art.doc, {},
644
+ 'Doc comment is overwritten by another one below' );
645
+ }
646
+ token.parsed = 'doc';
647
+ const val = !this.options.docComment || parseDocComment( token.text );
648
+ art.doc = { val, location: token.location };
649
+ }
650
+ }
651
+ }
652
+
653
+ setNullability( art, val, location = this.lb().location ) {
654
+ const notNull = { val, location };
655
+ if (art.notNull) {
656
+ this.reportDuplicateClause( 'notNull', art.notNull, notNull,
657
+ (val ? 'not null' : 'null') );
658
+ }
659
+ art.notNull = notNull;
660
+ }
661
+
662
+ setAssocAndComposition( art, assoc, card, target ) {
663
+ const { location } = assoc;
664
+ art.type = {
665
+ path: [ { id: keywordTypeNames[assoc.keyword], location } ],
666
+ scope: 'global',
667
+ location,
668
+ };
669
+ art.target = target;
670
+ if (!card)
671
+ return;
672
+
673
+ const targetMax = (card.keyword === 'one')
674
+ ? { val: 1, literal: 'number', location: card.location }
675
+ : { val: '*', literal: 'string', location: card.location };
676
+ // TODO: `literal` needed?
677
+ if (art.cardinality) {
678
+ this.reportDuplicateClause( 'cardinality', targetMax, art.cardinality.targetMax,
679
+ card.keyword, true );
680
+ }
681
+ else {
682
+ art.cardinality = { targetMax, location: targetMax.location };
683
+ }
684
+ }
685
+
686
+ reportExpandInline( column, isInline ) {
687
+ const { name } = column;
688
+ if (column.value && !column.value.path) {
689
+ const token = this.la();
690
+ // improve error location when using "inline" `.{…}` after ref (arguments and
691
+ // filters not covered, not worth the effort); after an expression where
692
+ // the last token is an identifier, not the `.` is wrong, but the `{`:
693
+ // if (isInline && !name && this._input.LT(-1).type >= this.constructor.Identifier)
694
+ // token = this._input.LT(2); -- TODO: still valid?
695
+ this.error( 'syntax-unexpected-nested-proj', token,
696
+ { code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }' },
697
+ 'Unexpected $(CODE); nested projections can only be used after a reference' );
698
+ // continuation semantics:
699
+ // - add elements anyway (could lead to duplicate errors as usual)
700
+ // - no errors for refs inside expand/inline, but for refs in sibling expr
701
+ // - think about: reference to these (sub) elements from other view
702
+ }
703
+ if (isInline && name) {
704
+ const alias = this.tokens[this.tokenIdx - 2];
705
+ const location = (isInline === true)
706
+ ? alias.location
707
+ : this.combineLocation( isInline, alias );
708
+ this.error( 'syntax-unexpected-alias', location, { code: '.{ ‹inline› }' },
709
+ 'Unexpected alias name before $(CODE)' );
710
+ // continuation semantics: ignore AS
711
+ }
712
+ }
713
+
714
+ reportDuplicateClause( prop, erroneous, chosen, code, literalValIfNotEq ) {
715
+ // probably easier for message linters not to use (?:) for the message id...?
716
+ const args = {
717
+ '#': prop,
718
+ code,
719
+ line: chosen.location.line,
720
+ col: chosen.location.col,
721
+ };
722
+ if (erroneous.val === chosen.val) {
723
+ this.warning( 'syntax-duplicate-equal-clause', erroneous.location, args );
724
+ }
725
+ else {
726
+ if (literalValIfNotEq)
727
+ args.code = chosen.val;
728
+ this.message( 'syntax-duplicate-clause', erroneous.location, args );
729
+ }
730
+ }
731
+
732
+ setTypeFacet( art, name, value ) {
733
+ const { text } = name;
734
+ if (text !== 'length' && text !== 'scale' && text !== 'precision' && text !== 'srid') {
735
+ this.error( 'syntax-undefined-param', name.location, { name: text },
736
+ 'There is no type parameter called $(NAME)');
737
+ }
738
+ else {
739
+ if (art[text] !== undefined)
740
+ this.error( 'syntax-duplicate-argument', art[text].location, { '#': 'type', name: text } );
741
+ // continuation semantics: use last
742
+ art[text] = value;
743
+ }
744
+ }
745
+
746
+ locationOfPrevTokens( offset ) {
747
+ const { file, line, col } = this.tokens[this.tokenIdx - offset].location;
748
+ const { endLine, endCol } = this.lb().location;
749
+ return {
750
+ file,
751
+ line,
752
+ col,
753
+ endLine,
754
+ endCol,
755
+ };
756
+ }
757
+
758
+ // TODO: also define method `combineWith` in Location
759
+ combineLocation( { location: start }, { location: end } = this.lb() ) {
760
+ const { file, line, col } = start;
761
+ // eslint-disable-next-line object-curly-newline
762
+ return { file, line, col, endLine: end.endLine, endCol: end.endCol };
763
+ }
764
+
765
+ // TODO: rename to `valAst`
766
+ valueWithLocation( val = undefined, token = this.lb() ) {
767
+ if (val === undefined)
768
+ val = token.keyword ?? token.text;
769
+ return { val, location: token.location };
770
+ }
771
+
772
+ surroundByParens( expr, open = this.lr(), close = this.lb() ) {
773
+ expr.$parens ??= [];
774
+ expr.$parens.push( this.combineLocation( open, close ) );
775
+ return expr;
776
+ }
777
+
778
+ // make sure that the parens of `IN (…)` do not disappear:
779
+ // TODO: make this a to-csn thing
780
+ secureParens( expr ) {
781
+ const op = expr?.op?.val;
782
+ const $parens = expr?.$parens;
783
+ if (!$parens || expr.query || op && op !== 'call' && op !== 'cast')
784
+ return expr;
785
+ // ensure that references, literals and functions keep their surrounding parentheses
786
+ // (is for expressions the case anyway)
787
+ const location = $parens.pop();
788
+ if (!$parens.length)
789
+ delete expr.$parens;
790
+ return {
791
+ op: { val: 'xpr', location: this.startLocation() },
792
+ args: [ expr ],
793
+ location,
794
+ };
795
+ }
796
+
797
+ pushXprToken( expr ) {
798
+ const token = this.lb();
799
+ (expr.args ?? expr).push( {
800
+ val: token.keyword ?? token.type,
801
+ location: token.location,
802
+ literal: 'token',
803
+ } );
804
+ }
805
+
806
+ applyOpToken( expr, nary = null ) {
807
+ const token = this.lb();
808
+ const op = { val: token.keyword ?? token.type, location: token.location, literal: 'token' };
809
+ if (nary === 'nary' && !expr?.$parens) {
810
+ const { args } = expr;
811
+ const prev = args?.[1];
812
+ if (prev?.val === op.val && prev?.literal === 'token') {
813
+ args.push( op );
814
+ return expr;
815
+ }
816
+ }
817
+ return {
818
+ op: { val: nary ?? 'ixpr', location: token.location },
819
+ args: (expr ? [ expr, op ] : [ op ] ),
820
+ };
821
+ }
822
+
823
+ valuePathAst( ref ) {
824
+ // TODO: XSN representation of functions is a bit strange - rework
825
+ // TODO: rework this function
826
+ const { path } = ref;
827
+ if (path?.length === 1) {
828
+ const { args, id, location } = path[0];
829
+ if (args
830
+ ? path[0].$syntax === ':'
831
+ : path[0].$delimited || !functionsWithoutParens.includes( id.toUpperCase() ))
832
+ return this.attachLocation( ref );
833
+
834
+ if (location.tokenIndex != null)
835
+ this.tokens[location.tokenIndex].parsed = 'func';
836
+ // TODO: XSN representation of functions is a bit strange - rework
837
+ const op = { location, val: 'call' };
838
+ return this.attachLocation( { op, func: ref, args } );
839
+ }
840
+
841
+ // $syntax === ':' => path(P: 1)
842
+ // $syntax !== ':' => path(P => 1) or path(1) or path()
843
+ const firstFunc = path.findIndex( i => i.args && i.$syntax !== ':' );
844
+ if (firstFunc === -1) // also covers empty paths
845
+ return ref;
846
+
847
+ // Method Call ---------------------------
848
+ // Transform the path into `.`-operators.
849
+ // Everything after the first function is also a function, and not a reference.
850
+
851
+ for (let i = firstFunc; i < path.length; ++i) {
852
+ if (path[i].args && path[i].$syntax === ':') {
853
+ // Error for `a(P => 1).b.c(P: 1)`: no ref after function.
854
+ this.error( 'syntax-invalid-ref', path[i].args[$location], {
855
+ code: '=>',
856
+ }, 'References after function calls can\'t be resolved. Use $(CODE) in function arguments');
857
+ break;
858
+ }
859
+ }
860
+
861
+ const args = [];
862
+ if (firstFunc > 0) {
863
+ args.push({
864
+ path: path.slice(0, firstFunc),
865
+ location: this.combineLocation( path[0], path[path.length - 1] ),
866
+ });
867
+ }
868
+
869
+ const pathRest = path.slice(firstFunc);
870
+ for (const method of pathRest) {
871
+ if (method !== pathRest[0] || firstFunc > 0) {
872
+ args.push({
873
+ // TODO: Update parser to have proper location for `.`?
874
+ location: this.startLocation( method ),
875
+ val: '.',
876
+ literal: 'token',
877
+ });
878
+ }
879
+ this.tokens[method.location.tokenIndex].parsed = 'func';
880
+ const func = {
881
+ op: { location: method.location, val: 'call' },
882
+ func: { path: [ method ] },
883
+ location: method.location,
884
+ };
885
+ if (method.args)
886
+ func.args = method.args;
887
+ args.push(func);
888
+ }
889
+
890
+ return {
891
+ op: { val: 'ixpr', location: this.startLocation() },
892
+ args,
893
+ location: ref.location,
894
+ };
895
+ }
896
+
897
+ associationInSelectItem( art ) {
898
+ const { value } = art;
899
+ const path = value?.path;
900
+ // we cannot compare "just one token before `:`" because there might be annos
901
+ if (path && path.length === 1 && !art.name && !art.expand && !art.inline) {
902
+ const name = value.path[0];
903
+ if (path.length === 1 && !name.args && !name.cardinality && !name.where) {
904
+ art.name = name;
905
+ delete art.value;
906
+ return;
907
+ }
908
+ }
909
+ this.error( 'syntax-unexpected-assoc', this.getCurrentToken(), {},
910
+ 'Unexpected association definition in select item' );
911
+ }
912
+
913
+ // must be in action directly after having parsed '{', '(`, or a keyword before
914
+ createDict( start ) {
915
+ const dict = Object.create(null);
916
+ dict[$location] = this.startLocation( start || this.lb() );
917
+ return dict;
918
+ }
919
+
920
+ // must be in action directly after having parsed '[' or '(` or `{`
921
+ createArray( start ) {
922
+ const array = [];
923
+ array[$location] = this.startLocation( start || this.lb() );
924
+ return array;
925
+ }
926
+
927
+ // must be in action directly after having parsed '}' or ')`
928
+ finalizeDictOrArray( dict ) {
929
+ const loc = dict[$location];
930
+ if (!loc)
931
+ return;
932
+ const stop = this.lb().location;
933
+ loc.endLine = stop.endLine;
934
+ loc.endCol = stop.endCol;
935
+ }
936
+
937
+ finalizeExtensionsDict( dict ) {
938
+ this.finalizeDictOrArray( dict );
939
+ for (const name in dict) {
940
+ const def = dict[name];
941
+ if (!def.$duplicates)
942
+ continue;
943
+
944
+ if (def.kind !== 'annotate') {
945
+ const numDefines
946
+ = def.$duplicates.reduce( addOneForDefinition, addOneForDefinition( 0, def ) );
947
+ this.handleDuplicateExtension( def, name, numDefines );
948
+ for (const dup of def.$duplicates)
949
+ this.handleDuplicateExtension( dup, name, numDefines );
950
+ continue;
951
+ }
952
+ // move annotations, 'doc' and 'elements' etc to main member
953
+ for (const dup of def.$duplicates) {
954
+ for (const prop of Object.keys( dup )) {
955
+ if (prop.charAt(0) === '@') {
956
+ this.addAnnotation( def, prop, dup[prop] );
957
+ delete dup[prop]; // we want to keep $duplicates, but not have duplicate props
958
+ }
959
+ else if (prop === 'doc') {
960
+ // With explicit docComment:false, we don't emit a warning.
961
+ if (def.doc && this.options.docComment !== false) {
962
+ this.warning( 'syntax-duplicate-doc-comment', def.doc.location, {},
963
+ 'Doc comment is overwritten by another one below' );
964
+ }
965
+ def.doc = dup.doc;
966
+ delete dup[prop]; // we want to keep $duplicates for LSP, but not have duplicate props
967
+ }
968
+ else if (extensionDicts[prop]) {
969
+ if (def[prop])
970
+ this.message( 'syntax-duplicate-annotate', [ def.name.location ], { name, prop } );
971
+ def[prop] = dup[prop]; // continuation semantics: last wins
972
+ delete dup[prop]; // we want to keep $duplicates for LSP, but not have duplicate props
973
+ }
974
+ }
975
+ if (dup.$annotations) { // update deprecated $annotations for cds-lsp / annotation modeler
976
+ if (def.$annotations)
977
+ def.$annotations.push( ...dup.$annotations );
978
+ else
979
+ def.$annotations = dup.$annotations;
980
+ }
981
+ }
982
+ // We keep duplicate statements for LSP, as it needs to traverse all
983
+ // identifiers; annotations were removed above to avoid traversing
984
+ // annotations twice.
985
+ }
986
+ }
987
+
988
+ /**
989
+ * Handle duplicate extensions. Does not handle `annotate`.
990
+ *
991
+ * @param {XSN.Extension} ext
992
+ * @param {string} name
993
+ * @param {number} numDefines
994
+ */
995
+ handleDuplicateExtension( ext, name, numDefines ) {
996
+ if (ext.kind === 'extend') {
997
+ this.error( 'syntax-duplicate-extend', [ ext.name.location ],
998
+ { name, '#': (numDefines ? 'define' : 'extend') } );
999
+ }
1000
+ else if (numDefines === 1) {
1001
+ ext.$errorReported = 'syntax-duplicate-extend';
1002
+ } // a definition, but not duplicate
1003
+ }
1004
+
1005
+ // Add new definition `art` to dictionary property `env` of node `parent`.
1006
+ // Return `art`.
1007
+ //
1008
+ // If argument `kind` is provided, set `art.kind` to that value.
1009
+ // If argument `name` is provided, set `art.name`:
1010
+ // - if `name` is an array, `name.id` consist of the ID of the last array item
1011
+ // (for elements via columns, foreign keys, table aliases)
1012
+ // - if `name` is an object, `name.id` is either set, or the (local) name is calculated
1013
+ // from the IDs of all items in `name.path` (for main artifact definitions).
1014
+ addDef( art, parent, env, kind, name ) {
1015
+ if (Array.isArray(name)) {
1016
+ const last = name.length && name[name.length - 1];
1017
+ art.name = { // A.B.C -> 'C'
1018
+ id: last?.id || '', location: last.location, $inferred: 'as',
1019
+ };
1020
+ }
1021
+ else if (name) {
1022
+ art.name = name;
1023
+ if (!name.id && kind === null) {
1024
+ name.id = name.variant
1025
+ ? `${ pathName( name.path ) }#${ pathName( name.variant.path ) }`
1026
+ : pathName( name.path );
1027
+ }
1028
+ }
1029
+ else {
1030
+ art.name = { id: '' };
1031
+ }
1032
+ if (kind)
1033
+ art.kind = kind;
1034
+
1035
+ const id = art.name?.id || pathName( art.name?.path ); // returns '' for corrupted name
1036
+
1037
+ parent[env] ??= Object.create(null);
1038
+ if (env === 'artifacts' || env === 'vocabularies') {
1039
+ dictAddArray( parent[env], id, art );
1040
+ }
1041
+ else if (kind || this.options.parseOnly) { // TODO: do not check parseOnly
1042
+ dictAdd( parent[env], id, art );
1043
+ }
1044
+ else {
1045
+ dictAdd( parent[env], id, art, ( duplicateName, loc ) => {
1046
+ // do not use function(), otherwise `this` is wrong:
1047
+ if (kind === 0) {
1048
+ this.error( 'syntax-duplicate-argument', loc, { name: duplicateName },
1049
+ 'Duplicate value for parameter $(NAME)' );
1050
+ }
1051
+ else if (kind === '') {
1052
+ this.error( 'syntax-duplicate-excluding', loc,
1053
+ { name: duplicateName, keyword: 'excluding' } );
1054
+ }
1055
+ else {
1056
+ this.error( 'syntax-duplicate-property', loc, { name: duplicateName },
1057
+ 'Duplicate value for structure property $(NAME)' );
1058
+ }
1059
+ } );
1060
+ }
1061
+ return art;
1062
+ }
1063
+
1064
+ /**
1065
+ * Add `annotate/extend Main.Artifact:elem.sub` to `‹xsn›.extensions`:
1066
+ * - the array item is an extend/annotate for `Main.Artifact`,
1067
+ * - for each path item in `elem.sub`, we add an `elements` property containing
1068
+ * one extend/annotate for the corresponding element
1069
+ * - The deepest extend/annotate is the object which is to be extended
1070
+ *
1071
+ * @param {object} ext The object containing the location and annotations for the extension.
1072
+ * @param {object} parent The parent containing the `extensions` property, i.e. the source.
1073
+ * @param {string} kind Either `annotate` or `extend`.
1074
+ * @param {object} artName The "name object" for `Main.Artifact`.
1075
+ * @param {XSN.Path} elemPath Path as returned by `simplePath` rule.
1076
+ */
1077
+ addExtension( ext, parent, kind, artName, elemPath ) {
1078
+ const { location } = ext;
1079
+ if (!Array.isArray( elemPath ) || !elemPath.length) {
1080
+ ext.kind = kind;
1081
+ ext.name = artName;
1082
+ parent.extensions.push( ext );
1083
+ return;
1084
+ }
1085
+ // Note: the element extensions share a common `location`, also with the
1086
+ // extension of the main artifact; its end location will usually set later
1087
+ const main = { kind, name: artName, location };
1088
+ parent.extensions.push( main );
1089
+ parent = main;
1090
+
1091
+ const last = elemPath[elemPath.length - 1];
1092
+ for (const seg of elemPath) {
1093
+ parent.elements = Object.create(null); // no dict location → no createDict()
1094
+ parent = this.addDef( (seg === last ? ext : { location }),
1095
+ parent, 'elements', kind, seg );
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ function addOneForDefinition( count, ext ) {
1101
+ return (ext.kind === 'extend') ? count : count + 1;
1102
+ }
1103
+
1104
+ // Significant digits (before exponent) without leading and trailing zeros
1105
+ function relevantDigits( val ) {
1106
+ const init = /^[-+0.]+/g; // global flag to have lastIndex
1107
+ const zeros = /[0.]+/g;
1108
+ if (init.test( val )) // sets init.lastIndex
1109
+ zeros.lastIndex = init.lastIndex;
1110
+
1111
+ let r;
1112
+ while ((r = zeros.exec( val )) != null &&
1113
+ zeros.lastIndex < val.length &&
1114
+ val.charAt( zeros.lastIndex ).toLowerCase() !== 'e')
1115
+ ;
1116
+ return val.slice( init.lastIndex, r?.index ).replace( /\./, '' );
1117
+ }
1118
+
1119
+
1120
+ // For compatibility with ANTLR-based parser:
1121
+ function antlrName( type ) {
1122
+ if (typeof type !== 'string')
1123
+ type = (!type.parsed || type.parsed === 'keyword') && type.keyword || type.type;
1124
+ if (/^[A-Z]+/.test( type ))// eslint-disable-next-line no-nested-ternary
1125
+ return (type === 'Id') ? 'Identifier' : (type === 'EOF') ? '<EOF>' : type;
1126
+ return (/^[a-z]+/.test( type )) ? type.toUpperCase() : `'${ type }'`;
1127
+ }
1128
+
1129
+ // Used for sorting in messages (TODO: make it part of messages.js?)
1130
+ const token1sort = {
1131
+ // 0: Identifier, Number, ...
1132
+ // 1: separators:
1133
+ ',': 1,
1134
+ '.': 1,
1135
+ ':': 1,
1136
+ ';': 1,
1137
+ // 2: parentheses:
1138
+ '(': 2,
1139
+ ')': 2,
1140
+ '[': 2,
1141
+ ']': 2,
1142
+ '{': 2,
1143
+ '}': 2,
1144
+ // 3: special:
1145
+ '!': 3,
1146
+ '#': 3,
1147
+ $: 3,
1148
+ '?': 3,
1149
+ '@': 3,
1150
+ // 4: operators:
1151
+ '*': 4,
1152
+ '+': 4,
1153
+ '-': 4,
1154
+ '/': 4,
1155
+ '<': 4,
1156
+ '=': 4,
1157
+ '>': 4,
1158
+ '|': 4,
1159
+ // 8: KEYWORD
1160
+ // 9: <EOF>
1161
+ };
1162
+
1163
+ function tokenPrecedence( name ) {
1164
+ if (name.length < 2 || name === '<EOF>')
1165
+ return `9${ name }`;
1166
+ const prec = token1sort[name.charAt(1)];
1167
+ if (prec)
1168
+ return `${ prec }${ name }`;
1169
+ return (name.charAt(1) < 'a' ? '8' : '0') + name;
1170
+ }
1171
+
1172
+ module.exports = AstBuildingParser;