@sap/cds-compiler 3.4.2 → 3.5.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 (143) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +1 -0
  3. package/bin/cds_update_identifiers.js +5 -5
  4. package/bin/cdsc.js +15 -16
  5. package/bin/cdshi.js +19 -6
  6. package/doc/CHANGELOG_ARCHIVE.md +2 -2
  7. package/doc/CHANGELOG_BETA.md +9 -1
  8. package/doc/CHANGELOG_DEPRECATED.md +2 -0
  9. package/lib/api/main.js +61 -59
  10. package/lib/api/options.js +4 -2
  11. package/lib/api/validate.js +2 -2
  12. package/lib/base/cleanSymbols.js +2 -3
  13. package/lib/base/dictionaries.js +6 -6
  14. package/lib/base/error.js +2 -2
  15. package/lib/base/keywords.js +6 -6
  16. package/lib/base/location.js +11 -12
  17. package/lib/base/message-registry.js +177 -58
  18. package/lib/base/messages.js +252 -180
  19. package/lib/base/model.js +14 -11
  20. package/lib/base/node-helpers.js +9 -10
  21. package/lib/base/optionProcessorHelper.js +138 -129
  22. package/lib/checks/.eslintrc.json +2 -0
  23. package/lib/checks/actionsFunctions.js +5 -5
  24. package/lib/checks/annotationsOData.js +4 -4
  25. package/lib/checks/arrayOfs.js +1 -1
  26. package/lib/checks/cdsPersistence.js +1 -1
  27. package/lib/checks/checkForTypes.js +3 -3
  28. package/lib/checks/defaultValues.js +3 -3
  29. package/lib/checks/elements.js +7 -7
  30. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  31. package/lib/checks/foreignKeys.js +1 -1
  32. package/lib/checks/invalidTarget.js +4 -4
  33. package/lib/checks/managedInType.js +1 -1
  34. package/lib/checks/managedWithoutKeys.js +1 -1
  35. package/lib/checks/nonexpandableStructured.js +5 -3
  36. package/lib/checks/nullableKeys.js +1 -1
  37. package/lib/checks/onConditions.js +5 -6
  38. package/lib/checks/parameters.js +1 -1
  39. package/lib/checks/queryNoDbArtifacts.js +2 -2
  40. package/lib/checks/selectItems.js +4 -4
  41. package/lib/checks/sql-snippets.js +4 -4
  42. package/lib/checks/types.js +7 -7
  43. package/lib/checks/utils.js +4 -4
  44. package/lib/checks/validator.js +16 -13
  45. package/lib/compiler/.eslintrc.json +4 -1
  46. package/lib/compiler/assert-consistency.js +8 -7
  47. package/lib/compiler/builtins.js +14 -14
  48. package/lib/compiler/checks.js +123 -48
  49. package/lib/compiler/define.js +12 -13
  50. package/lib/compiler/extend.js +266 -60
  51. package/lib/compiler/finalize-parse-cdl.js +10 -5
  52. package/lib/compiler/index.js +17 -14
  53. package/lib/compiler/populate.js +14 -6
  54. package/lib/compiler/propagator.js +2 -0
  55. package/lib/compiler/resolve.js +2 -15
  56. package/lib/compiler/shared.js +27 -16
  57. package/lib/compiler/tweak-assocs.js +5 -6
  58. package/lib/compiler/utils.js +20 -0
  59. package/lib/edm/annotations/genericTranslation.js +604 -358
  60. package/lib/edm/annotations/preprocessAnnotations.js +39 -35
  61. package/lib/edm/csn2edm.js +275 -222
  62. package/lib/edm/edm.js +17 -3
  63. package/lib/edm/edmAnnoPreprocessor.js +6 -6
  64. package/lib/edm/edmInboundChecks.js +2 -2
  65. package/lib/edm/edmPreprocessor.js +107 -77
  66. package/lib/edm/edmUtils.js +44 -5
  67. package/lib/gen/Dictionary.json +210 -8
  68. package/lib/gen/language.checksum +1 -1
  69. package/lib/gen/language.interp +67 -63
  70. package/lib/gen/language.tokens +81 -81
  71. package/lib/gen/languageLexer.interp +4 -10
  72. package/lib/gen/languageLexer.js +854 -869
  73. package/lib/gen/languageLexer.tokens +79 -81
  74. package/lib/gen/languageParser.js +14309 -13832
  75. package/lib/inspect/inspectModelStatistics.js +2 -2
  76. package/lib/inspect/inspectPropagation.js +6 -6
  77. package/lib/inspect/inspectUtils.js +2 -2
  78. package/lib/json/from-csn.js +102 -55
  79. package/lib/json/to-csn.js +119 -198
  80. package/lib/language/antlrParser.js +5 -2
  81. package/lib/language/docCommentParser.js +6 -6
  82. package/lib/language/errorStrategy.js +43 -23
  83. package/lib/language/genericAntlrParser.js +113 -133
  84. package/lib/language/language.g4 +1550 -1506
  85. package/lib/language/multiLineStringParser.js +3 -3
  86. package/lib/language/textUtils.js +2 -2
  87. package/lib/main.js +3 -3
  88. package/lib/model/csnRefs.js +5 -0
  89. package/lib/model/csnUtils.js +130 -122
  90. package/lib/model/revealInternalProperties.js +1 -1
  91. package/lib/model/sortViews.js +4 -6
  92. package/lib/modelCompare/compare.js +2 -2
  93. package/lib/modelCompare/utils/.eslintrc.json +22 -0
  94. package/lib/modelCompare/utils/filter.js +100 -0
  95. package/lib/optionProcessor.js +5 -0
  96. package/lib/render/.eslintrc.json +1 -0
  97. package/lib/render/DuplicateChecker.js +1 -1
  98. package/lib/render/manageConstraints.js +12 -12
  99. package/lib/render/toCdl.js +311 -276
  100. package/lib/render/toHdbcds.js +97 -94
  101. package/lib/render/toRename.js +5 -5
  102. package/lib/render/toSql.js +127 -223
  103. package/lib/render/utils/common.js +141 -108
  104. package/lib/render/utils/delta.js +227 -0
  105. package/lib/render/utils/sql.js +22 -6
  106. package/lib/render/utils/stringEscapes.js +3 -3
  107. package/lib/transform/db/.eslintrc.json +2 -0
  108. package/lib/transform/db/applyTransformations.js +3 -3
  109. package/lib/transform/db/assertUnique.js +13 -12
  110. package/lib/transform/db/associations.js +5 -5
  111. package/lib/transform/db/cdsPersistence.js +10 -8
  112. package/lib/transform/db/constraints.js +14 -14
  113. package/lib/transform/db/expansion.js +20 -22
  114. package/lib/transform/db/flattening.js +24 -42
  115. package/lib/transform/db/groupByOrderBy.js +3 -3
  116. package/lib/transform/db/temporal.js +6 -6
  117. package/lib/transform/db/transformExists.js +23 -23
  118. package/lib/transform/db/views.js +16 -16
  119. package/lib/transform/draft/.eslintrc.json +1 -35
  120. package/lib/transform/draft/db.js +10 -10
  121. package/lib/transform/draft/odata.js +2 -2
  122. package/lib/transform/forOdataNew.js +8 -29
  123. package/lib/transform/forRelationalDB.js +16 -6
  124. package/lib/transform/localized.js +11 -10
  125. package/lib/transform/odata/toFinalBaseType.js +41 -27
  126. package/lib/transform/odata/typesExposure.js +113 -47
  127. package/lib/transform/parseExpr.js +209 -106
  128. package/lib/transform/transformUtilsNew.js +17 -10
  129. package/lib/transform/translateAssocsToJoins.js +24 -19
  130. package/lib/transform/universalCsn/coreComputed.js +10 -10
  131. package/lib/transform/universalCsn/universalCsnEnricher.js +26 -26
  132. package/lib/transform/universalCsn/utils.js +3 -3
  133. package/lib/utils/file.js +5 -5
  134. package/lib/utils/moduleResolve.js +13 -13
  135. package/lib/utils/objectUtils.js +6 -6
  136. package/lib/utils/term.js +5 -2
  137. package/lib/utils/timetrace.js +51 -24
  138. package/package.json +5 -8
  139. package/share/messages/check-proper-type-of.md +1 -1
  140. package/share/messages/message-explanations.json +1 -1
  141. package/share/messages/redirected-to-complex.md +4 -4
  142. package/share/messages/{syntax-expecting-integer.md → syntax-expecting-unsigned-int.md} +7 -4
  143. package/lib/modelCompare/filter.js +0 -83
@@ -55,8 +55,11 @@ function match( ttype ) {
55
55
  const token = this.getCurrentToken();
56
56
  if (token.type === identType || !keywordRegexp.test( token.text ))
57
57
  return antlr4.Parser.prototype.match.call( this, ttype );
58
-
59
- this.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text } );
58
+ // This is very likely to be dead code: we do not use a simple Identifier
59
+ // without alternatives in the grammar. With alternatives, recoverInline() is
60
+ // the place to go. (But this code should work with a changed grammar…)
61
+ this.message( 'syntax-unexpected-reserved-word', token,
62
+ { code: token.text, delimited: token.text } );
60
63
  this._errHandler.reportMatch(this);
61
64
  this.consume();
62
65
  return token;
@@ -72,7 +75,6 @@ class KeywordErrorStrategy extends DefaultErrorStrategy {
72
75
  super( ...args );
73
76
 
74
77
  this._super = {
75
- consumeUntil: super.consumeUntil,
76
78
  recoverInline: super.recoverInline,
77
79
  getExpectedTokens: super.getExpectedTokens,
78
80
  };
@@ -89,6 +91,7 @@ Object.assign( KeywordErrorStrategy.prototype, {
89
91
  reportIgnoredWith,
90
92
  // getErrorRecoverySet,
91
93
  consumeUntil,
94
+ consumeAndMarkUntil,
92
95
  recoverInline,
93
96
  getMissingSymbol,
94
97
  getExpectedTokensForMessage,
@@ -137,16 +140,19 @@ function sync( recognizer ) {
137
140
  }
138
141
 
139
142
  // Expected token is identifier, current is (reserved) KEYWORD:
140
- // TODO: do not use this if "close enough" (1 char diff) to a keyword in nextTokens
143
+ // TODO: do not use this if "close enough" (1 char diff or prefix)
144
+ // to a keyword in nextTokens
141
145
  //
142
146
  // NOTE: it is important to do this only if EPSILON is not in `nextTokens`,
143
- // which means that we cannot bring the better special syntax-fragile-ident
147
+ // which means that we cannot bring the better special syntax-unexpected-reserved
144
148
  // in all cases. Reason: high performance impact of the alternative,
145
149
  // i.e. calling method Parser#isExpectedToken() = invoking the ATN
146
150
  // interpreter to see behind EPSILON.
147
151
  const identType = recognizer.constructor.Identifier;
148
152
  if (keywordRegexp.test( token.text ) && nextTokens.contains( identType )) {
149
- recognizer.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text } );
153
+ recognizer.message( 'syntax-unexpected-reserved-word', token,
154
+ { code: token.text, delimited: token.text } );
155
+ // TODO: attach tokens like for 'syntax-unexpected-token'
150
156
  token.type = identType; // make next ANTLR decision assume identifier
151
157
  return;
152
158
  }
@@ -210,11 +216,11 @@ function reportInputMismatch( recognizer, e, deadEnds ) {
210
216
  const expecting = deadEnds !== true && // true: cannot compute expecting
211
217
  this.getExpectedTokensForMessage( recognizer, e.offendingToken, deadEnds );
212
218
  const offending = this.getTokenDisplay( e.offendingToken, recognizer );
219
+ e.offendingToken.$isSkipped = 'offending';
213
220
  let err;
214
221
  if (expecting && expecting.length) {
215
- err = recognizer.error( 'syntax-mismatched-token', e.offendingToken,
216
- { offending, expecting },
217
- 'Mismatched $(OFFENDING), expecting $(EXPECTING)' );
222
+ err = recognizer.error( 'syntax-unexpected-token', e.offendingToken,
223
+ { offending, expecting } );
218
224
  err.expectedTokens = expecting;
219
225
  }
220
226
  else { // should not really happen anymore... -> no messageId !
@@ -232,11 +238,12 @@ function reportUnwantedToken( recognizer ) {
232
238
  this.beginErrorCondition(recognizer);
233
239
 
234
240
  const token = recognizer.getCurrentToken();
241
+ token.$isSkipped = 'offending';
235
242
  const expecting = this.getExpectedTokensForMessage( recognizer, token );
236
243
  const offending = this.getTokenDisplay( token, recognizer );
237
- const err = recognizer.error( 'syntax-extraneous-token', token,
238
- { offending, expecting },
239
- 'Extraneous $(OFFENDING), expecting $(EXPECTING)' );
244
+ // Just text variant, no other message id! Would depend on ANTLR-internals
245
+ const err = recognizer.error( 'syntax-unexpected-token', token,
246
+ { '#': 'unwanted', offending, expecting } );
240
247
  err.expectedTokens = expecting; // TODO: remove next token?
241
248
  if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig
242
249
  recognizer.notifyErrorListeners( err.message, token, err );
@@ -249,9 +256,11 @@ function reportMissingToken( recognizer ) {
249
256
  this.beginErrorCondition(recognizer);
250
257
 
251
258
  const token = recognizer.getCurrentToken();
259
+ token.$isSkipped = 'offending';
252
260
  const expecting = this.getExpectedTokensForMessage( recognizer, token );
253
261
  const offending = this.getTokenDisplay( token, recognizer );
254
262
  // TODO: if non-reserved keyword will not been parsed as keyword, use Identifier for offending
263
+ // Hopefully not too ANTLR-specific, so extra message id is ok:
255
264
  const err = recognizer.error( 'syntax-missing-token', token,
256
265
  { offending, expecting },
257
266
  'Missing $(EXPECTING) before $(OFFENDING)' );
@@ -264,10 +273,10 @@ function reportIgnoredWith( recognizer, t ) {
264
273
  const next = recognizer._interp.atn.states[recognizer.state].transitions[0].target;
265
274
  recognizer.state = next.stateNumber; // previous match() does not set the state
266
275
  const expecting = this.getExpectedTokensForMessage( recognizer, t );
267
- const m = recognizer.warning( 'syntax-ignored-with', t,
268
- { offending: "';'", expecting },
276
+ const m = recognizer.warning( 'syntax-unexpected-semicolon', t,
277
+ { offending: "';'", expecting, keyword: 'with' },
269
278
  // eslint-disable-next-line max-len
270
- 'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous WITH' );
279
+ 'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
271
280
  m.expectedTokens = expecting;
272
281
  }
273
282
 
@@ -280,10 +289,10 @@ function consumeUntil( recognizer, set ) {
280
289
 
281
290
  // let s=this.getTokenDisplay( recognizer.getCurrentToken(), recognizer );
282
291
  if (SEMI < 1 || RBRACE < 1) {
283
- this._super.consumeUntil.call( this, recognizer, set );
292
+ this.consumeAndMarkUntil( recognizer, set );
284
293
  }
285
294
  else if (set.contains(SEMI)) { // do not check for RBRACE here!
286
- this._super.consumeUntil.call( this, recognizer, set );
295
+ this.consumeAndMarkUntil( recognizer, set );
287
296
  // console.log('CONSUMED-ORIG:',s,this.getTokenDisplay( recognizer.getCurrentToken(),
288
297
  // recognizer ),recognizer.getCurrentToken().line,intervalSetToArray( recognizer, set ));
289
298
  }
@@ -295,9 +304,9 @@ function consumeUntil( recognizer, set ) {
295
304
  stop.addOne( SEMI );
296
305
  // I am not that sure whether to add RBRACE...
297
306
  stop.addOne( RBRACE );
298
- this._super.consumeUntil.call( this, recognizer, stop );
299
- if (recognizer.getTokenStream().LA(1) === SEMI ||
300
- recognizer.getTokenStream().LA(1) === RBRACE && !set.contains(RBRACE)) {
307
+ this.consumeAndMarkUntil( recognizer, stop );
308
+ const ttype = recognizer.getTokenStream().LA(1);
309
+ if (ttype === SEMI || ttype === RBRACE && !set.contains(RBRACE)) {
301
310
  recognizer.consume();
302
311
  this.reportMatch(recognizer); // we know current token is correct
303
312
  }
@@ -312,6 +321,15 @@ function consumeUntil( recognizer, set ) {
312
321
  }
313
322
  }
314
323
 
324
+ function consumeAndMarkUntil( recognizer, set ) {
325
+ let t = recognizer.getTokenStream().LT(1);
326
+ while (t.type !== antlr4.Token.EOF && !set.contains( t.type )) {
327
+ if (!t.$isSkipped)
328
+ t.$isSkipped = true;
329
+ recognizer.consume();
330
+ t = recognizer.getTokenStream().LT(1);
331
+ }
332
+ }
315
333
 
316
334
  // As the `match` function of the parser `recognizer` does not allow to check
317
335
  // against a set of token types, the generated parser code checks against that
@@ -328,7 +346,9 @@ function recoverInline( recognizer ) {
328
346
  if (!keywordRegexp.test( token.text ))
329
347
  return this._super.recoverInline.call( this, recognizer );
330
348
 
331
- recognizer.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text } );
349
+ // TODO: attach `Identifier` as valid name to message?
350
+ recognizer.message( 'syntax-unexpected-reserved-word', token,
351
+ { code: token.text, delimited: token.text } );
332
352
  this.reportMatch(recognizer); // we know current token is correct
333
353
  recognizer.consume();
334
354
  return token;
@@ -496,14 +516,14 @@ function getExpectedTokensForMessage( recognizer, offendingToken, deadEnds ) {
496
516
  // expected.toString(recognizer.literalNames, recognizer.symbolicNames));
497
517
  return intervalSetToArray( recognizer, expected );
498
518
 
499
- function addSet(other) {
519
+ function addSet( other ) {
500
520
  if (!other.contains( hideAltsType ))
501
521
  origAddSet.call( this, other );
502
522
  }
503
523
 
504
524
  // Add an interval `v` to the IntervalSet `this`. If `v` contains the token
505
525
  // type `Identifier`, do not add non-reserved keywords in `v`.
506
- function addInterval(v) {
526
+ function addInterval( v ) {
507
527
  if (v.stop <= identType) {
508
528
  origAddInterval.call(this, v);
509
529
  }
@@ -86,14 +86,15 @@ Object.assign(GenericAntlrParser.prototype, {
86
86
  previousTokenAtLocation,
87
87
  combinedLocation,
88
88
  surroundByParens,
89
+ secureParens,
89
90
  unaryOpForParens,
90
91
  leftAssocBinaryOp,
91
92
  classifyImplicitName,
93
+ warnIfColonFollows,
92
94
  fragileAlias,
93
95
  identAst,
94
- functionAst,
95
- setLastAsXpr,
96
- xprToken,
96
+ pushXprToken,
97
+ argsExpression,
97
98
  valuePathAst,
98
99
  signedExpression,
99
100
  numberLiteral,
@@ -109,7 +110,6 @@ Object.assign(GenericAntlrParser.prototype, {
109
110
  createArray,
110
111
  finalizeDictOrArray,
111
112
  createPrefixOp,
112
- setOnce,
113
113
  setMaxCardinality,
114
114
  pushIdent,
115
115
  handleComposition,
@@ -117,7 +117,6 @@ Object.assign(GenericAntlrParser.prototype, {
117
117
  reportExpandInline,
118
118
  checkTypeFacet,
119
119
  csnParseOnly,
120
- disallowElementExtension,
121
120
  noAssignmentInSameLine,
122
121
  noSemicolonHere,
123
122
  setLocalToken,
@@ -217,29 +216,12 @@ function setLocalTokenForId( tokenNameMap ) {
217
216
  // // throw new antlr4.error.InputMismatchException(this);
218
217
  // }
219
218
 
220
- /**
221
- * For element extensions (`extend E:elem` syntax).
222
- * If `elemName.path` is set, remove the last extension from `$outer` and
223
- * emit an error that the extension is invalid.
224
- *
225
- * @param {object} elemName
226
- * @param {object} outer
227
- * @param {string} extensionVariant
228
- */
229
- function disallowElementExtension(elemName, outer, extensionVariant) {
230
- if (elemName.path) {
231
- const loc = this.tokenLocation(this.getCurrentToken());
232
- this.error( 'syntax-invalid-extend', loc, { kind: extensionVariant } );
233
- // remove last, i.e. new extension
234
- outer.extensions.pop();
235
- }
236
- }
237
-
238
219
  function noAssignmentInSameLine() {
239
220
  const t = this.getCurrentToken();
240
221
  if (t.text === '@' && t.line <= this._input.LT(-1).line) {
241
- this.warning( 'syntax-anno-same-line', t, {},
242
- 'Annotation assignment belongs to next statement' );
222
+ this.warning( 'syntax-missing-newline', t, { anno: '‹anno›' }, // TODO: single quotes, @()
223
+ // eslint-disable-next-line max-len
224
+ 'Add a newline before $(ANNO) to indicate that it belongs to the next statement' );
243
225
  }
244
226
  }
245
227
 
@@ -267,9 +249,15 @@ const genericTokenTypes = {
267
249
  intro: 'GenericIntro',
268
250
  };
269
251
 
270
- function prepareGenericKeywords( pathItem, expected = null) {
252
+ /**
253
+ * @memberOf GenericAntlrParser
254
+ *
255
+ * @param pathItem
256
+ * @param [expected]
257
+ */
258
+ function prepareGenericKeywords( pathItem, expected = null ) {
271
259
  const length = pathItem?.args?.length || 0;
272
- const argPos = (expected ? length - 1 : length);
260
+ const argPos = length;
273
261
  const func = pathItem?.id && specialFunctions[pathItem.id.toUpperCase()];
274
262
  const spec = func && func[argPos] || specialFunctions[''][argPos ? 1 : 0];
275
263
  this.$genericKeywords = spec;
@@ -492,10 +480,10 @@ function tokenLocation( token, endToken = null ) {
492
480
  return loc;
493
481
  }
494
482
 
495
- function isMultiLineToken(token) {
483
+ function isMultiLineToken( token ) {
496
484
  return (
497
485
  token.type === this.constructor.DocComment ||
498
- token.type === this.constructor.String ||
486
+ token.type === this.constructor.String || // TODO: do not check every string content
499
487
  token.type === this.constructor.UnterminatedLiteral
500
488
  );
501
489
  }
@@ -565,6 +553,23 @@ function combinedLocation( start, end ) {
565
553
  return locUtils.combinedLocation( start, end );
566
554
  }
567
555
 
556
+ // make sure that the parens of `IN (…)` do not disappear:
557
+ function secureParens( expr ) {
558
+ const op = expr?.op?.val;
559
+ const $parens = expr?.$parens;
560
+ if (!$parens || expr.query || op && op !== 'call' && op !== 'cast')
561
+ return expr;
562
+ // ensure that references, literals and functions keep their surrounding parentheses
563
+ // (is for expressions the case anyway)
564
+ delete expr.$parens;
565
+ return {
566
+ op: { val: 'xpr', location: this.startLocation() },
567
+ args: [ expr ],
568
+ location: { ...expr.location },
569
+ $parens,
570
+ };
571
+ }
572
+
568
573
  function surroundByParens( expr, open, close, asQuery = false ) {
569
574
  if (!expr)
570
575
  return expr;
@@ -584,6 +589,17 @@ function unaryOpForParens( query, val ) {
584
589
  return { op: { val, location }, location, args: [ query ] };
585
590
  }
586
591
 
592
+ // ANTLR on some OS might corrupt non-ASCII chars for messages
593
+ function warnIfColonFollows( anno ) {
594
+ const t = this.getCurrentToken();
595
+ if (t.text === ':') {
596
+ this.warning( 'syntax-missing-parens', anno.name.location,
597
+ { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
598
+ // eslint-disable-next-line max-len
599
+ 'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
600
+ }
601
+ }
602
+
587
603
  // If the token before the current one is a doc comment (ignoring other tokens
588
604
  // on the hidden channel), put its "cleaned-up" text as value of property `doc`
589
605
  // of arg `node` (which could be an array). Complain if `doc` is already set.
@@ -611,9 +627,9 @@ function docComment( node ) {
611
627
 
612
628
  // Classify token (identifier category) for implicit names,
613
629
  // to be used in the empty alternative to AS <explicitName>.
614
- function classifyImplicitName( category, ref ) {
630
+ function classifyImplicitName( category, ref, tokpos = 1 ) {
615
631
  if (!ref || ref.path && this.getCurrentToken().text !== '.') {
616
- const implicit = this._input.LT(-1);
632
+ const implicit = this._input.LT( tokpos - 1 || -1 );
617
633
  if (implicit.isIdentifier)
618
634
  implicit.isIdentifier = category;
619
635
  }
@@ -623,11 +639,11 @@ function fragileAlias( ast, safe = false ) {
623
639
  if (this.getCurrentToken().text === '.')
624
640
  return ast;
625
641
  if (safe || ast.$delimited || !/^[a-zA-Z][a-zA-Z_]+$/.test( ast.id )) {
626
- this.warning( 'syntax-sloppy-alias', ast.location, { keyword: 'as' },
642
+ this.warning( 'syntax-deprecated-auto-as', ast.location, { keyword: 'as' },
627
643
  'Please add the keyword $(KEYWORD) in front of the alias name' );
628
644
  }
629
645
  else { // configurable error
630
- this.message( 'syntax-fragile-alias', ast.location, { keyword: 'as' },
646
+ this.message( 'syntax-missing-as', ast.location, { keyword: 'as' },
631
647
  'Please add the keyword $(KEYWORD) in front of the alias name' );
632
648
  }
633
649
  return ast;
@@ -642,10 +658,9 @@ function identAst( token, category, noTokenTypeCheck = false ) {
642
658
  id = '';
643
659
  if (token.text[0] === '!') {
644
660
  id = id.slice( 2, -1 ).replace( /]]/g, ']' );
645
- if (!id) {
646
- this.error( 'syntax-empty-ident', token, {},
647
- 'Delimited identifier must contain at least one character' );
648
- }
661
+ if (!id)
662
+ this.message( 'syntax-invalid-name', token, {} );
663
+
649
664
  // $delimited is used to complain about ![$self] and other magic vars usage;
650
665
  // we might complain about that already here via @arg{category}
651
666
  return { id, $delimited: true, location: this.tokenLocation( token ) };
@@ -655,8 +670,7 @@ function identAst( token, category, noTokenTypeCheck = false ) {
655
670
  // delimited:
656
671
  id = id.slice( 1, -1 ).replace( /""/g, '"' );
657
672
  if (!id) {
658
- this.error( 'syntax-empty-ident', token, {},
659
- 'Delimited identifier must contain at least one character' );
673
+ this.message( 'syntax-invalid-name', token, {} );
660
674
  }
661
675
  else {
662
676
  this.message( 'syntax-deprecated-ident', token, { delimited: id },
@@ -666,33 +680,27 @@ function identAst( token, category, noTokenTypeCheck = false ) {
666
680
  return { id, $delimited: true, location: this.tokenLocation( token ) };
667
681
  }
668
682
 
669
- function functionAst( token, xpr ) { // only for special function TRIM and EXTRACT
670
- // TODO: XSN func cleanup
671
- const location = this.tokenLocation( token );
672
- const args = xpr
673
- ? [ { op: { location, val: 'ixpr' }, args: [], location: this.tokenLocation( xpr ) } ]
674
- : [];
675
- return {
676
- op: { location, val: 'call' },
677
- func: { path: [ { id: token.text, location, args } ], location },
678
- args,
679
- location,
680
- };
681
- }
682
-
683
- function setLastAsXpr( args ) {
684
- const pos = args.length - 1;
685
- const last = args[pos];
686
- if (!last || last?.op?.val === 'ixpr') // actuall an internal xpr, which is always flattened
687
- return last;
688
- const location = { ...last.location };
689
- args[pos] = { op: { location, val: 'ixpr' }, args: [ last ], location };
690
- return args[pos].args;
683
+ // only to be used in @after
684
+ // TODO: remove compatible stuff (A2J/checks use op: 'and'/'=')
685
+ function argsExpression( args, useFirstLocation, compatible = null ) {
686
+ // console.log('AE:',args);
687
+ if (args.length === 1)
688
+ return args[0];
689
+ const location = useFirstLocation && args[0] && { ...args[0].location };
690
+ const op = compatible && args.length === 3 && args[1].val === compatible && args[1];
691
+ const expr = (op)
692
+ ? { op: { val: op.val, location: op.location }, args: [ args[0], args[2] ], location }
693
+ : { op: { val: 'ixpr', location: this.startLocation() }, args, location };
694
+ return this.attachLocation( expr );
691
695
  }
692
696
 
693
- function xprToken() {
697
+ function pushXprToken( args ) {
694
698
  const token = this._input.LT(-1);
695
- return { location: this.tokenLocation( token ), val: token.text, literal: 'token' };
699
+ args.push( {
700
+ location: this.tokenLocation( token ),
701
+ val: token.text.toLowerCase(), // TODO: remove toLowerCase() ?
702
+ literal: 'token',
703
+ } );
696
704
  }
697
705
 
698
706
  function valuePathAst( ref ) {
@@ -705,7 +713,7 @@ function valuePathAst( ref ) {
705
713
  const item = path.find( i => i.args && i.$syntax !== ':' );
706
714
  if (!item)
707
715
  return ref;
708
- this.error( 'syntax-not-supported', item.location, {},
716
+ this.error( 'syntax-unsupported-method', item.location, {},
709
717
  'Methods in expressions are not supported yet' );
710
718
  path.broken = true;
711
719
  path.length = 1;
@@ -729,10 +737,11 @@ function valuePathAst( ref ) {
729
737
 
730
738
  // If a '-' is directly before an unsigned number, consider it part of the number;
731
739
  // otherwise (including for '+'), represent it as extra unary prefix operator.
732
- function signedExpression( signToken, expr ) {
733
- const sign = this.valueWithTokenLocation( signToken.text, signToken );
740
+ function signedExpression( args, expr ) {
741
+ // if (args.length !== 1) throw Error()
742
+ const sign = args[0];
734
743
  const nval
735
- = (signToken.text === '-' &&
744
+ = (sign.val === '-' &&
736
745
  expr && // expr may be null if `-` rule can't be parsed
737
746
  expr.literal === 'number' &&
738
747
  sign.location.endLine === expr.location.line &&
@@ -740,11 +749,14 @@ function signedExpression( signToken, expr ) {
740
749
  (typeof expr.val === 'number'
741
750
  ? expr.val >= 0 && -expr.val
742
751
  : !expr.val.startsWith('-') && `-${ expr.val }`)) || false;
743
- if (nval === false)
744
- return { op: sign, args: expr ? [ expr ] : [] };
745
- expr.val = nval;
746
- --expr.location.col;
747
- return expr;
752
+ if (nval === false) {
753
+ args.push( expr );
754
+ }
755
+ else {
756
+ expr.val = nval;
757
+ --expr.location.col;
758
+ args[0] = expr;
759
+ }
748
760
  }
749
761
 
750
762
  // Return AST for number token `token` with optional token `sign`. Represent
@@ -765,7 +777,7 @@ function numberLiteral( token, sign, text = token.text ) {
765
777
  const num = Number.parseFloat( text || '0' ); // not Number.parseInt() !
766
778
  if (!Number.isSafeInteger(num)) {
767
779
  if (sign == null) {
768
- this.error( 'syntax-expecting-integer', token,
780
+ this.error( 'syntax-expecting-unsigned-int', token,
769
781
  { '#': !text.match(/^[0-9]*$/) ? 'normal' : 'unsafe' } );
770
782
  }
771
783
 
@@ -819,7 +831,7 @@ function quotedLiteral( token, literal ) {
819
831
  location,
820
832
  };
821
833
 
822
- function atChar(i) {
834
+ function atChar( i ) {
823
835
  // Is only used with single-line strings.
824
836
  return location.col + pos + i;
825
837
  }
@@ -843,7 +855,7 @@ function pushIdent( path, ident, prefix ) {
843
855
  endLine: ident.location.line,
844
856
  endCol: ident.location.col,
845
857
  };
846
- this.error( 'syntax-anno-space', wsLocation, {}, // TODO: really Error?
858
+ this.error( 'syntax-unexpected-space', wsLocation, {}, // TODO: really Error?
847
859
  'Expected identifier after \'@\' but found whitespace' );
848
860
  }
849
861
  ident.location.line = tokenLoc.line;
@@ -900,15 +912,15 @@ function addDef( art, parent, env, kind, name ) {
900
912
  dictAdd( parent[env], art.name.id, art, ( duplicateName, loc ) => {
901
913
  // do not use function(), otherwise `this` is wrong:
902
914
  if (kind === 0) {
903
- this.error( 'duplicate-argument', loc, { name: duplicateName },
915
+ this.error( 'syntax-duplicate-argument', loc, { name: duplicateName },
904
916
  'Duplicate value for parameter $(NAME)' );
905
917
  }
906
918
  else if (kind === '') {
907
- this.error( 'duplicate-excluding', loc, { name: duplicateName, keyword: 'excluding' },
908
- 'Duplicate $(NAME) in the $(KEYWORD) clause' );
919
+ this.error( 'syntax-duplicate-excluding', loc,
920
+ { name: duplicateName, keyword: 'excluding' } );
909
921
  }
910
922
  else {
911
- this.error( 'duplicate-prop', loc, { name: duplicateName },
923
+ this.error( 'syntax-duplicate-property', loc, { name: duplicateName },
912
924
  'Duplicate value for structure property $(NAME)' );
913
925
  }
914
926
  } );
@@ -1026,33 +1038,14 @@ function leftAssocBinaryOp( left, opToken, eToken, right, extraProp = 'quantifie
1026
1038
  return { op, args: [ left, right ], location: left.location };
1027
1039
  }
1028
1040
 
1029
- // Set property `prop` of `target` to value `value`. Issue error if that
1030
- // property has been set before, while mentioning the keywords previously
1031
- // provided (as arguments `tokens`).
1032
- function setOnce( target, prop, value, ...tokens ) {
1033
- const loc = this.tokenLocation( tokens[0], tokens[tokens.length - 1] );
1034
- const prev = target[prop];
1035
- if (prev) {
1036
- this.error( 'syntax-repeated-option', loc, { option: prev.option },
1037
- 'Option $(OPTION) has already been specified' );
1038
- }
1039
- if (typeof value === 'boolean') {
1040
- if (!value)
1041
- loc.value = false;
1042
- value = loc;
1043
- }
1044
- value.option = tokens.map( t => t.text.toUpperCase() ).join(' ');
1045
- target[prop] = value;
1046
- }
1047
-
1048
1041
  function setMaxCardinality( art, token, max ) {
1049
1042
  const location = this.tokenLocation( token );
1050
1043
  if (!art.cardinality) {
1051
1044
  art.cardinality = { targetMax: Object.assign( { location }, max ), location };
1052
1045
  }
1053
1046
  else {
1054
- this.warning( 'syntax-repeated-cardinality', location, { keyword: token.text },
1055
- 'The target cardinality has already been specified - ignored $(KEYWORD)' );
1047
+ this.warning( 'syntax-duplicate-cardinality', location, { keyword: token.text },
1048
+ 'The target cardinality has already been specified - ignoring $(KEYWORD)' );
1056
1049
  }
1057
1050
  }
1058
1051
 
@@ -1069,33 +1062,20 @@ function handleComposition( cardinality, isComposition ) {
1069
1062
  }
1070
1063
 
1071
1064
  function associationInSelectItem( art ) {
1072
- if (!art.value) // e.g. `expand` without value (for new structures)
1073
- return;
1074
-
1075
- const isPath = art.value.path && art.value.path.length;
1076
- const isIdentifier = isPath && art.value.path.length === 1;
1077
- if (isIdentifier) {
1078
- if (!art.name) {
1079
- art.name = art.value.path[0];
1080
- }
1081
- else {
1082
- // Use alias if provided, i.e. ignore art.value.path.
1083
- this.error( 'query-unexpected-alias', art.name.location, {},
1084
- 'Unexpected alias for association' );
1085
- }
1086
- delete art.value;
1087
- }
1088
- else {
1089
- const loc = isPath ? art.value.path[1].location : art.value.location;
1090
- // If neither path nor alias are present, `query-req-name` is emitted in `populate.js`.
1091
- if (isPath || art.name) {
1092
- this.error( 'query-expected-identifier', loc, { '#': 'assoc' } );
1093
- if (isPath)
1094
- art.name = art.value.path[art.value.path.length - 1];
1095
-
1065
+ const { value } = art;
1066
+ const path = value?.path;
1067
+ // we cannot compare "just one token before `:`" because there might be annos
1068
+ if (path && path.length === 1 && !art.name && !art.expand && !art.inline) {
1069
+ const name = value.path[0];
1070
+ if (path.length === 1 && !name.args && !name.cardinality && !name.where) {
1071
+ art.name = name;
1096
1072
  delete art.value;
1073
+ return art;
1097
1074
  }
1098
1075
  }
1076
+ this.error( 'syntax-unexpected-assoc', this.getCurrentToken(), {},
1077
+ 'Unexpected association definition in select item' );
1078
+ return {}; // result of the association rules are written into /dev/null
1099
1079
  }
1100
1080
 
1101
1081
  function reportExpandInline( column, isInline ) {
@@ -1108,8 +1088,8 @@ function reportExpandInline( column, isInline ) {
1108
1088
  if (isInline && !name && this._input.LT(-1).type >= this.constructor.Identifier)
1109
1089
  token = this._input.LT(2);
1110
1090
  this.error( 'syntax-unexpected-nested-proj', token,
1111
- { prop: isInline ? 'inline' : 'expand' },
1112
- { std: 'Unexpected nested $(PROP), can only be used after a reference' } );
1091
+ { code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }' },
1092
+ 'Unexpected $(CODE); nested projections can only be used after a reference' );
1113
1093
  // continuation semantics:
1114
1094
  // - add elements anyway (could lead to duplicate errors as usual)
1115
1095
  // - no errors for refs inside expand/inline, but for refs in sibling expr
@@ -1117,25 +1097,25 @@ function reportExpandInline( column, isInline ) {
1117
1097
  }
1118
1098
  if (isInline && name) {
1119
1099
  const location = this.tokenLocation( isInline, this._input.LT(-1) );
1120
- this.error( 'syntax-unexpected-alias', location, { prop: 'inline' },
1121
- 'Unexpected alias name before nested $(PROP)' );
1100
+ this.error( 'syntax-unexpected-alias', location, { code: '.{ ‹inline› }' },
1101
+ 'Unexpected alias name before $(CODE)' );
1122
1102
  // continuation semantics: ignore AS
1123
1103
  }
1124
1104
  }
1125
1105
 
1126
1106
  function checkTypeFacet( art, argIdent ) {
1107
+ // TODO: use dictAddArray or dictAdd?
1127
1108
  const { id } = argIdent;
1128
1109
  if (id === 'length' || id === 'scale' || id === 'precision' || id === 'srid') {
1129
1110
  if (art[id] !== undefined) {
1130
- this.error( 'syntax-duplicate-argument', argIdent.location,
1131
- { '#': 'duplicate', code: id } );
1132
1111
  this.error( 'syntax-duplicate-argument', art[id].location,
1133
- { '#': 'duplicate', code: id } );
1112
+ { '#': 'type', name: id } );
1113
+ // continuation semantics: use last
1134
1114
  }
1135
1115
  return true;
1136
1116
  }
1137
- this.error( 'syntax-duplicate-argument', argIdent.location,
1138
- { '#': 'unknown', code: id } );
1117
+ this.error( 'syntax-undefined-param', argIdent.location, { name: id },
1118
+ 'There is no type parameter called $(NAME)');
1139
1119
  return false;
1140
1120
  }
1141
1121