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