@sap/cds-compiler 5.3.2 → 5.4.2

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 (49) hide show
  1. package/CHANGELOG.md +29 -2
  2. package/bin/cdsc.js +1 -1
  3. package/doc/CHANGELOG_BETA.md +2 -2
  4. package/lib/api/options.js +4 -2
  5. package/lib/base/builtins.js +0 -10
  6. package/lib/base/keywords.js +3 -31
  7. package/lib/base/message-registry.js +23 -5
  8. package/lib/base/messages.js +1 -1
  9. package/lib/checks/existsMustEndInAssoc.js +7 -2
  10. package/lib/checks/foreignKeys.js +12 -7
  11. package/lib/compiler/assert-consistency.js +11 -3
  12. package/lib/compiler/builtins.js +2 -0
  13. package/lib/compiler/checks.js +88 -38
  14. package/lib/compiler/define.js +2 -2
  15. package/lib/compiler/shared.js +9 -10
  16. package/lib/compiler/xpr-rewrite.js +11 -0
  17. package/lib/compiler/xsn-model.js +1 -1
  18. package/lib/edm/csn2edm.js +2 -0
  19. package/lib/edm/edm.js +2 -1
  20. package/lib/edm/edmPreprocessor.js +14 -1
  21. package/lib/edm/edmUtils.js +17 -2
  22. package/lib/gen/BaseParser.js +291 -197
  23. package/lib/gen/CdlParser.js +1631 -1605
  24. package/lib/gen/Dictionary.json +74 -6
  25. package/lib/gen/language.checksum +1 -1
  26. package/lib/gen/language.interp +1 -1
  27. package/lib/gen/languageParser.js +1808 -1804
  28. package/lib/language/antlrParser.js +8 -4
  29. package/lib/language/genericAntlrParser.js +3 -3
  30. package/lib/model/csnUtils.js +6 -1
  31. package/lib/optionProcessor.js +4 -0
  32. package/lib/parsers/AstBuildingParser.js +172 -108
  33. package/lib/parsers/CdlGrammar.g4 +154 -134
  34. package/lib/parsers/Lexer.js +3 -3
  35. package/lib/parsers/identifiers.js +59 -0
  36. package/lib/render/toCdl.js +5 -5
  37. package/lib/render/utils/common.js +5 -0
  38. package/lib/render/utils/delta.js +23 -5
  39. package/lib/transform/db/expansion.js +2 -1
  40. package/lib/transform/db/transformExists.js +10 -9
  41. package/lib/transform/effective/annotations.js +147 -0
  42. package/lib/transform/effective/main.js +16 -2
  43. package/lib/transform/forOdata.js +53 -10
  44. package/lib/transform/forRelationalDB.js +7 -0
  45. package/lib/transform/odata/createForeignKeys.js +180 -0
  46. package/lib/transform/odata/flattening.js +135 -19
  47. package/lib/transform/odata/typesExposure.js +4 -3
  48. package/lib/transform/transformUtils.js +6 -6
  49. package/package.json +1 -1
@@ -275,15 +275,19 @@ function parseWithNewParser( source, filename, options, messageFunctions, rule )
275
275
  // TODO: what should we do in case of errors?
276
276
  $ctx.stop = this.lb();
277
277
  parseListener.exitEveryRule( $ctx );
278
- CdlParser.prototype.exit_.apply( this, args );
278
+ return CdlParser.prototype.exit_.apply( this, args );
279
279
  };
280
280
  parser.c = function c( ...args ) { // consume
281
- CdlParser.prototype.c.apply( this, args );
282
- parseListener.visitTerminal( { symbol: this.lb() } );
281
+ const symbol = this.la();
282
+ const result = CdlParser.prototype.c.apply( this, args );
283
+ if (result)
284
+ parseListener.visitTerminal( { symbol } );
285
+ return result;
283
286
  };
284
287
  parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery
288
+ const symbol = this.la();
285
289
  CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx`
286
- parseListener.visitErrorNode( { symbol: this.lb() } );
290
+ parseListener.visitErrorNode( { symbol } );
287
291
  };
288
292
  }
289
293
  const result = {};
@@ -17,7 +17,7 @@ const {
17
17
  specialFunctions,
18
18
  quotedLiteralPatterns,
19
19
  } = require('../compiler/builtins');
20
- const { functionsWithoutParens } = require('../base/builtins');
20
+ const { functionsWithoutParentheses } = require('../parsers/identifiers');
21
21
  const { Location } = require('../base/location');
22
22
  const { pathName } = require('../compiler/utils');
23
23
  const { XsnArtifact, XsnName, XsnSource } = require('../compiler/xsn-model');
@@ -728,7 +728,7 @@ function docComment( node ) {
728
728
  */
729
729
  function classifyImplicitName( category, ref ) {
730
730
  if (!ref || ref.path) {
731
- const tokenIndex = ref?.path[ref.path.length - 1]?.location.tokenIndex;
731
+ const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
732
732
  const implicit = (tokenIndex === undefined) ? this._input.LT(-1) : this._input.get(tokenIndex);
733
733
  if (implicit.isIdentifier) {
734
734
  const previous = implicit.isIdentifier;
@@ -847,7 +847,7 @@ function valuePathAst( ref ) {
847
847
  const { args, id, location } = path[0];
848
848
  if (args
849
849
  ? path[0].$syntax === ':'
850
- : path[0].$delimited || !functionsWithoutParens.includes( id.toUpperCase() ))
850
+ : path[0].$delimited || !functionsWithoutParentheses.includes( id.toUpperCase() ))
851
851
  return ref;
852
852
 
853
853
  const implicit = this.previousTokenAtLocation( location );
@@ -227,7 +227,12 @@ function getUtils( model, universalReady ) {
227
227
  * @returns {boolean}
228
228
  */
229
229
  function isStructured( obj ) {
230
- return !!(obj.elements || (obj.type && getFinalTypeInfo(obj.type)?.elements));
230
+ if (obj.elements)
231
+ return true;
232
+ if (!obj.type)
233
+ return false;
234
+ const typeInfo = getFinalTypeInfo(obj.type);
235
+ return !!(typeInfo?.elements); // TODO? `|| typeInfo?.type === 'cds.Map');`
231
236
  }
232
237
 
233
238
  // Return true if 'node' is a managed association element
@@ -603,6 +603,7 @@ optionProcessor.command('forEffective')
603
603
  optionProcessor.command('forSeal')
604
604
  .option('-h, --help')
605
605
  .option('--remap-odata-annotations <val>', { valid: ['true', 'false'] } )
606
+ .option('--derive-analytical-annotations <val>', { valid: ['true', 'false'] })
606
607
  .positionalArgument('<files...>')
607
608
  .help(`
608
609
  Usage: cdsc forSeal [options] <files...>
@@ -614,6 +615,9 @@ optionProcessor.command('forSeal')
614
615
  --remap-odata-annotations <val> Remap OData annotations to ABAP annotations:
615
616
  true: (default) remap annotations
616
617
  false: leave them as is
618
+ --derive-analytical-annotations <val> Set analytics annotations
619
+ true: set the annotations
620
+ false: (default) don't set them
617
621
  `);
618
622
 
619
623
  module.exports = {
@@ -4,7 +4,7 @@ const BaseParser = require( '../gen/BaseParser' );
4
4
 
5
5
  const { Location } = require( '../base/location' );
6
6
  const { dictAdd, dictAddArray } = require('../base/dictionaries');
7
- const { functionsWithoutParens } = require('../base/builtins');
7
+ const { functionsWithoutParentheses } = require('./identifiers');
8
8
 
9
9
  const { pathName } = require('../compiler/utils');
10
10
  const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
@@ -70,8 +70,8 @@ class AstBuildingParser extends BaseParser {
70
70
  return this.$messageFunctions.info( id, location?.location || location, args, text );
71
71
  }
72
72
 
73
- expectingArray() {
74
- const expecting = this._expecting();
73
+ expectingArray( token ) {
74
+ const expecting = this._expecting( token );
75
75
  let array = Object.keys( expecting );
76
76
  // compatibility: replace true+false by Boolean
77
77
  if (expecting.true && expecting.false)
@@ -81,7 +81,7 @@ class AstBuildingParser extends BaseParser {
81
81
  }
82
82
 
83
83
  reportUnexpectedToken_( token ) {
84
- const expecting = this.expectingArray();
84
+ const expecting = this.expectingArray( token );
85
85
  const err = this.error( 'syntax-unexpected-token', token,
86
86
  { offending: antlrName( token ), expecting } );
87
87
  // No 'unwanted' variant, no 'syntax-missing-token'
@@ -94,25 +94,53 @@ class AstBuildingParser extends BaseParser {
94
94
  err.expectedTokens = this.expectingArray();
95
95
  }
96
96
 
97
- setPrecInCallingRule() {
98
- const caller = this.stack.at( -1 );
99
- if (this.inSameRule_( caller.ruleState, caller.followState ))
100
- caller.prec = this.prec_;
97
+ reportInternalError_( token ) {
98
+ this.error( null, token, { offending: antlrName( token ) },
99
+ 'Mismatched $(OFFENDING); skipped one token' );
100
+ // TMP: should not happen anymore → remove method in redepage
101
+ throw new Error( 'Repeated error reporting with same token' );
101
102
  }
102
103
 
103
- tableAlias() {
104
+ tableWithoutAs() {
105
+ // TODO TOOL: if the tool properly creates `default: this.giR()`, this
106
+ // condition method is most likely not necessary
104
107
  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;
108
+ // TODO: if necessary, we could allow some keywords, and just make sure that
109
+ // all JOIN variants are still possible
110
+ return !keyword || this.keywords[keyword] == null;
111
+ }
112
+
113
+ /**
114
+ * Handle allowed mixes of expression categories.
115
+ *
116
+ * - <prepare=queryOnLeft>: define a new parentheses context if not direct
117
+ * recursive call, it can finally turn to be both a query or expr/table
118
+ * - <prepare=queryOnLeft, arg=‹SomeVal›>: make the current parentheses
119
+ * context to be not a query anymore
120
+ * - <cond=queryOnLeft> tests whether the expression on the left is a query
121
+ * - <cond=queryOnLeft, arg=‹SomeVal›>: tests whether the expression on the
122
+ * left is a query, then make the current context to be not a query anymore
123
+ * - <cond=queryOnLeft, arg=tableWithoutAs>: …after having checked
124
+ * whether the next token is no (reserved or unreserved) keyword
125
+ */
126
+ queryOnLeft( test, arg ) {
127
+ if (arg === 'tableWithoutAs') {
128
+ if (!this.tableWithoutAs())
129
+ return false;
130
+ }
131
+ else if (!arg && !test) {
132
+ // provide new dynamic parentheses context, except with direct
133
+ // recursive call:
134
+ if (this.inSameRule_( this.s, this.stack.at( -1 ).followState ))
135
+ return true;
136
+ this.dynamic_.parenthesesCtx = [ null ];
137
+ this._tracePush( 'Parentheses()' );
138
+ }
139
+ const { parenthesesCtx } = this.dynamic_;
140
+ const noQuery = parenthesesCtx?.[0];
141
+ if (arg && parenthesesCtx)
142
+ parenthesesCtx[0] = arg;
143
+ return !noQuery;
116
144
  }
117
145
 
118
146
  prepareSpecialFunction() {
@@ -133,7 +161,7 @@ class AstBuildingParser extends BaseParser {
133
161
  lGenericIntroOrExpr( tryGenericIntro = true ) {
134
162
  const { keyword, type } = this.la();
135
163
  // TODO: use lower-case in specialFunctions
136
- const text = keyword?.toUpperCase() ?? type;
164
+ const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
137
165
  const generic = this.dynamic_.generic?.[text];
138
166
  if (tryGenericIntro) {
139
167
  if (this.dynamic_.generic?.IN === 'separator')
@@ -142,7 +170,7 @@ class AstBuildingParser extends BaseParser {
142
170
  return (generic === 'intro') ? 'GenericIntro' : 'Id';
143
171
  const next = this.tokens[this.tokenIdx + 1];
144
172
  if (next.type !== ',' && next.type !== ')' &&
145
- this.dynamic_.generic[next.keyword?.toUpperCase()] !== 'separator')
173
+ this.dynamic_.generic[next.keyword?.toUpperCase?.()] !== 'separator')
146
174
  return 'GenericIntro';
147
175
  }
148
176
  return (generic === 'expr') ? 'GenericExpr' : 'Id';
@@ -155,7 +183,7 @@ class AstBuildingParser extends BaseParser {
155
183
  lGenericSeparator() {
156
184
  const { keyword, type } = this.la();
157
185
  // TODO: use lower-case in specialFunctions
158
- const text = keyword?.toUpperCase() ?? type;
186
+ const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
159
187
  const generic = this.dynamic_.generic?.[text];
160
188
  return (generic === 'separator') ? 'GenericSeparator' : ',';
161
189
  }
@@ -166,25 +194,32 @@ class AstBuildingParser extends BaseParser {
166
194
  return realTokens?.map( s => s.toLowerCase() ) ?? [];
167
195
  }
168
196
 
169
- isDotForPath() {
197
+ inSelectItem( _test, arg ) { // only as action
198
+ this.dynamic_.inSelectItem = arg;
199
+ }
200
+
201
+ /**
202
+ * `virtual` and `key` cannot be used inside expand/inline
203
+ * (also inside sub queries in those, which will be rejected later anyway)
204
+ */
205
+ modifierRestriction() {
206
+ return this.dynamic_.inSelectItem !== 'nested';
207
+ }
208
+
209
+ isDotForPath() { // see also inSelectItem
170
210
  if (this.dynamic_.inSelectItem == null)
171
211
  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
212
+ // TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
213
+ // (as <prepare>)
174
214
  const next = this.tokens[this.tokenIdx + 1].type;
175
215
  return next !== '*' && next !== '{';
176
216
  }
177
217
 
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;
218
+ // <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
219
+ // completion for `… default 3 not ~;` currently just `null` but hey
220
+ isNegatedRelation( _test, prec ) {
221
+ return this.tokens[this.tokenIdx + 1].keyword !== 'null' &&
222
+ this.precNone_( _test, prec );
188
223
  }
189
224
 
190
225
  isNamedArg() {
@@ -223,76 +258,53 @@ class AstBuildingParser extends BaseParser {
223
258
 
224
259
  /**
225
260
  * Prepare element restrictions and check validility of final anno assignments.
226
- * TODO TOOL: `arg` for actions/conditions
227
261
  *
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?
262
+ * Called as <prepare=…>:
235
263
  *
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.
264
+ * - <…, arg=default> in `returnsSpec`: after `returns`
265
+ * disallow `default` in `typeExpression`
266
+ * - <…,arg=elem> in `elementDef` (before calling `typeExpression`):
267
+ * allow `default`/`= calcExpr` with final annotation assignments,
268
+ * delay final doc comment
269
+ * - <…, arg=calc> in `typeExpression` (with associations, etc)
270
+ * now disallow `= calcExpr` (with annotation assignments) in `elementDef`,
271
+ * do not delay final doc comments anymore
272
+ * - <…, arg=anno> in `typeExpression` after enums:
273
+ * now disallow annotation assignments after `= calcExpr`,
274
+ * ignore doc comment after having called `typeExpression`
251
275
  *
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?
276
+ * Called as <cond=…>:
260
277
  *
261
- * To have any effect, <prepare=elementRestriction> must have been called.
278
+ * - <…, arg=default> in `nullabilityAndDefault`:
279
+ * is `default` allowed? If used, disallow calc (with anno assignments)
280
+ * - <…, arg=calc> in `elementDef`:
281
+ * is `= calcExpr` allowed? If so, check for final doc
282
+ * - <…, arg=anno> in `elementDef`:
283
+ * are annotation assignments after `= calcExpr` allowed?
262
284
  */
263
- calcOrDefaultRestriction( test, arg ) {
285
+ elementRestriction( test, arg ) {
264
286
  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
287
+ if (test) {
288
+ if (!elementCtx || elementCtx[0] === arg)
289
+ return !elementCtx;
290
+ if (arg === 'default') {
291
+ elementCtx[0] = 'calc';
292
+ this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
293
+ }
270
294
  }
271
- else if (this.l() === '=') { // <cond> before `= calcExpression`
272
- return elementCtx?.[0];
295
+ else if (arg === 'elem' || arg === 'default') {
296
+ this.dynamic_.elementCtx = [ arg ];
273
297
  }
274
- else { // <cond> before `default`
275
- if (elementCtx)
276
- elementCtx[0] = false;
277
- this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
298
+ else if (elementCtx) {
299
+ elementCtx[0] = arg;
278
300
  }
279
- return !elementCtx?.[2]; // default allowed?
301
+ return true;
280
302
  }
281
303
 
282
304
  inExpandInline() { // not as <cond>
283
305
  this.dynamic_.inSelectItem = 'nested';
284
306
  }
285
307
 
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
308
  /**
297
309
  * `;` between statements is optional only after a `}` (ex braces of structure
298
310
  * values for annotations).
@@ -314,20 +326,24 @@ class AstBuildingParser extends BaseParser {
314
326
  }
315
327
 
316
328
  /**
317
- * `...` can appear in the top-level array value only and not after `...`
318
- * without `up to`.
329
+ * - `{}` can only appears inside array-valued annotations
330
+ * - `...` can appear in the top-level array value only and not after `...`
331
+ * without `up to`.
319
332
  */
320
- ellipsisRestriction( test ) {
333
+ arrayAnno( test, arg ) {
321
334
  if (!test) {
322
335
  this.dynamic_.arrayAnno = [ !this.dynamic_.arrayAnno ];
323
336
  }
324
- else { // on '...'
337
+ else if (arg === 'ellipsis') { // on '...'
325
338
  const { arrayAnno } = this.dynamic_;
326
339
  if (!arrayAnno[0])
327
340
  return false;
328
341
  if (this.tokens[this.tokenIdx + 1]?.type === ',')
329
342
  arrayAnno[0] = false;
330
343
  }
344
+ else { // orNotEmpty
345
+ return this.dynamic_.arrayAnno || this.lb().type !== '{';
346
+ }
331
347
  return true;
332
348
  }
333
349
 
@@ -352,9 +368,9 @@ class AstBuildingParser extends BaseParser {
352
368
  this.error( msgId, location, textArgs );
353
369
  }
354
370
 
355
- warnIfColonFollows( anno ) {
371
+ warnIfColonFollows( name ) {
356
372
  if (this.l() === ':') {
357
- this.warning( 'syntax-missing-parens', anno.name,
373
+ this.warning( 'syntax-missing-parens', name,
358
374
  { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
359
375
  // eslint-disable-next-line @stylistic/js/max-len
360
376
  'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
@@ -370,7 +386,8 @@ class AstBuildingParser extends BaseParser {
370
386
  }
371
387
  }
372
388
 
373
- // For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths
389
+ // For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths,
390
+ // inside `.*` and `.{`
374
391
  reportUnexpectedSpace( prefix = this.lb(),
375
392
  location = this.la().location,
376
393
  isError = false ) {
@@ -469,9 +486,11 @@ class AstBuildingParser extends BaseParser {
469
486
  }
470
487
  if (!prefix) { // set deprecated $annotations for cds-lsp
471
488
  const { line, col } = name.location;
489
+ // prefer value end-location if it exists
490
+ const endLoc = val.location || val.name.location;
472
491
  const location = {
473
492
  __proto__: Location.prototype,
474
- ...val.location,
493
+ ...endLoc,
475
494
  line,
476
495
  col,
477
496
  };
@@ -542,7 +561,7 @@ class AstBuildingParser extends BaseParser {
542
561
 
543
562
  classifyImplicitName( category, ref ) {
544
563
  if (!ref || ref.path) {
545
- const tokenIndex = ref?.path[ref.path.length - 1]?.location.tokenIndex;
564
+ const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
546
565
  const token = this.prevTokenWithIndex( tokenIndex ) ?? this.tokens[this.tokenIdx - 1];
547
566
  const { parsedAs } = token;
548
567
  if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword') {
@@ -580,7 +599,8 @@ class AstBuildingParser extends BaseParser {
580
599
  let val;
581
600
 
582
601
  if (text.startsWith( '`' )) {
583
- val = parseMultiLineStringLiteral.call( this, token ); // TODO: remove `call()` syntax
602
+ val = token.keyword !== 0 && // 0 -> unterminated literal
603
+ parseMultiLineStringLiteral.call( this, token ); // TODO: remove `call()` syntax
584
604
  }
585
605
  else {
586
606
  pos = text.search( '\'' ) + 1; // pos of char after quote
@@ -717,8 +737,20 @@ class AstBuildingParser extends BaseParser {
717
737
  * - misplaced doc comments would lead to a parse error (incompatible),
718
738
  * - would influence the prediction and error recovery,
719
739
  * - is only slightly "more declarative" in the grammar.
740
+ *
741
+ * With argument `delayed`, potentially delay the doc processing.
742
+ * See also `elementRestriction`.
720
743
  */
721
- docComment( art ) {
744
+ docComment( art, delayed = undefined ) {
745
+ if (delayed !== this.dynamic_?.[0]) {
746
+ if (delayed === 'type')
747
+ return;
748
+ }
749
+ else if (delayed === 'elem') {
750
+ this.dynamic_[0] = 'type';
751
+ return;
752
+ }
753
+
722
754
  const { line: prevLine, col: prevCol } = this.lb()?.location ?? { line: 0, col: 0 };
723
755
  const { line: currLine, col: currCol } = this.la().location;
724
756
  let token;
@@ -726,6 +758,7 @@ class AstBuildingParser extends BaseParser {
726
758
  token = this.docComments[this.docCommentIndex];
727
759
  if (!token)
728
760
  return; // no further doc comment
761
+ // TODO: we could use location.tokenIndex
729
762
  const { line, col } = token.location;
730
763
  if (art && (line > currLine || line === currLine && col > currCol))
731
764
  return; // next doc comment after current token
@@ -750,6 +783,21 @@ class AstBuildingParser extends BaseParser {
750
783
  }
751
784
  }
752
785
 
786
+ checkWith( keyword ) {
787
+ if (this.lb() !== keyword)
788
+ return;
789
+ const tok = this.la();
790
+ if (this.docCommentIndex < tok.location.tokenIndex &&
791
+ this.docCommentIndex > this.lb().location.tokenIndex)
792
+ return;
793
+ const expecting = this.expectingArray(); // TODO: filter
794
+ const msg = this.warning( 'syntax-unexpected-semicolon', tok,
795
+ { offending: antlrName( tok ), expecting, keyword: 'with' },
796
+ // eslint-disable-next-line @stylistic/js/max-len
797
+ 'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
798
+ msg.expectedTokens = expecting;
799
+ }
800
+
753
801
  setNullability( art, val, location = this.lb().location ) {
754
802
  const notNull = { val, location };
755
803
  if (art.notNull) {
@@ -843,7 +891,20 @@ class AstBuildingParser extends BaseParser {
843
891
  }
844
892
  }
845
893
 
894
+ // TODO: remove the check from the parser; move it to shared.js
895
+ checkTypeArgs( art ) {
896
+ const args = art.$typeArgs;
897
+ // One or two arguments are interpreted as either length or precision/scale.
898
+ if (args.length > 2) {
899
+ const loc = args[2].location;
900
+ this.error( 'syntax-unexpected-argument', loc, {}, 'Too many type arguments' );
901
+ art.$typeArgs.length = 0;
902
+ }
903
+ }
904
+
846
905
  locationOfPrevTokens( offset ) {
906
+ // TODO: use combined location of lb() and la() and move actions accordingly
907
+ // (for error recovery)
847
908
  const { file, line, col } = this.tokens[this.tokenIdx - offset].location;
848
909
  const { endLine, endCol } = this.lb().location;
849
910
  return {
@@ -912,7 +973,7 @@ class AstBuildingParser extends BaseParser {
912
973
 
913
974
  pushXprToken( expr ) {
914
975
  const token = this.lb();
915
- (expr.args ?? expr).push( {
976
+ (expr.args ?? expr).push?.( {
916
977
  val: token.keyword ?? token.type,
917
978
  location: token.location,
918
979
  literal: 'token',
@@ -922,7 +983,7 @@ class AstBuildingParser extends BaseParser {
922
983
  applyOpToken( expr, nary = null ) {
923
984
  const token = this.lb();
924
985
  const op = { val: token.keyword ?? token.type, location: token.location, literal: 'token' };
925
- if (nary === 'nary' && !expr?.$parens) {
986
+ if (nary === 'nary' && expr && !expr.$parens) {
926
987
  const { args } = expr;
927
988
  const prev = args?.[1];
928
989
  if (prev?.val === op.val && prev?.literal === 'token') {
@@ -944,7 +1005,7 @@ class AstBuildingParser extends BaseParser {
944
1005
  const { args, id, location } = path[0];
945
1006
  if (args
946
1007
  ? path[0].$syntax === ':'
947
- : path[0].$delimited || !functionsWithoutParens.includes( id.toUpperCase() ))
1008
+ : path[0].$delimited || !functionsWithoutParentheses.includes( id.toUpperCase() ))
948
1009
  return this.attachLocation( ref );
949
1010
 
950
1011
  const funcToken = this.prevTokenWithIndex( location.tokenIndex );
@@ -1013,11 +1074,11 @@ class AstBuildingParser extends BaseParser {
1013
1074
  }
1014
1075
 
1015
1076
  associationInSelectItem( art ) {
1016
- const { value } = art;
1017
- const path = value?.path;
1077
+ this.classifyImplicitName( 'ItemAssoc', art.value );
1078
+ const path = art.value?.path;
1018
1079
  // we cannot compare "just one token before `:`" because there might be annos
1019
1080
  if (path && path.length === 1 && !art.name && !art.expand && !art.inline) {
1020
- const name = value.path[0];
1081
+ const name = path[0];
1021
1082
  if (path.length === 1 && !name.args && !name.cardinality && !name.where) {
1022
1083
  art.name = name;
1023
1084
  delete art.value;
@@ -1130,6 +1191,9 @@ class AstBuildingParser extends BaseParser {
1130
1191
  // - if `name` is an object, `name.id` is either set, or the (local) name is calculated
1131
1192
  // from the IDs of all items in `name.path` (for main artifact definitions).
1132
1193
  addDef( art, parent, env, kind, name ) {
1194
+ if (!art)
1195
+ return art; // parser error
1196
+
1133
1197
  if (Array.isArray(name)) {
1134
1198
  const last = name.length && name[name.length - 1];
1135
1199
  art.name = { // A.B.C -> 'C'