@sap/cds-compiler 5.6.0 → 5.7.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 (55) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/bin/cdsse.js +1 -0
  3. package/bin/cdsv2m.js +2 -1
  4. package/doc/Versioning.md +4 -4
  5. package/lib/api/options.js +1 -0
  6. package/lib/base/builtins.js +2 -2
  7. package/lib/base/dictionaries.js +1 -2
  8. package/lib/base/keywords.js +3 -1
  9. package/lib/base/lazyload.js +1 -1
  10. package/lib/base/message-registry.js +169 -144
  11. package/lib/base/messages.js +69 -59
  12. package/lib/base/model.js +3 -3
  13. package/lib/base/node-helpers.js +17 -16
  14. package/lib/base/optionProcessorHelper.js +13 -14
  15. package/lib/base/shuffle.js +4 -1
  16. package/lib/checks/structuredAnnoExpressions.js +1 -1
  17. package/lib/compiler/assert-consistency.js +1 -1
  18. package/lib/compiler/builtins.js +2 -1
  19. package/lib/compiler/extend.js +20 -5
  20. package/lib/compiler/resolve.js +45 -9
  21. package/lib/compiler/shared.js +1 -0
  22. package/lib/edm/annotations/edmJson.js +3 -3
  23. package/lib/edm/annotations/genericTranslation.js +5 -1
  24. package/lib/edm/annotations/vocabularyDefinitions.js +2 -2
  25. package/lib/edm/edmUtils.js +2 -1
  26. package/lib/gen/BaseParser.js +32 -32
  27. package/lib/gen/CdlParser.js +1526 -1488
  28. package/lib/json/from-csn.js +2 -0
  29. package/lib/json/to-csn.js +13 -4
  30. package/lib/language/docCommentParser.js +11 -5
  31. package/lib/language/errorStrategy.js +3 -3
  32. package/lib/language/genericAntlrParser.js +2 -0
  33. package/lib/model/csnUtils.js +6 -1
  34. package/lib/optionProcessor.js +5 -1
  35. package/lib/parsers/AstBuildingParser.js +161 -73
  36. package/lib/parsers/CdlGrammar.g4 +129 -85
  37. package/lib/parsers/Lexer.js +5 -3
  38. package/lib/parsers/index.js +1 -1
  39. package/lib/render/toCdl.js +6 -5
  40. package/lib/render/toHdbcds.js +1 -1
  41. package/lib/render/toSql.js +5 -3
  42. package/lib/render/utils/common.js +19 -6
  43. package/lib/render/utils/delta.js +1 -3
  44. package/lib/render/utils/standardDatabaseFunctions.js +576 -0
  45. package/lib/transform/addTenantFields.js +2 -1
  46. package/lib/transform/db/flattening.js +18 -77
  47. package/lib/transform/db/groupByOrderBy.js +2 -2
  48. package/lib/transform/db/rewriteCalculatedElements.js +14 -19
  49. package/lib/transform/db/temporal.js +2 -1
  50. package/lib/transform/odata/adaptAnnotationRefs.js +79 -0
  51. package/lib/transform/odata/createForeignKeys.js +4 -71
  52. package/lib/transform/odata/flattening.js +11 -1
  53. package/lib/transform/transformUtils.js +20 -85
  54. package/package.json +2 -1
  55. package/bin/cds_update_annotations.js +0 -180
@@ -486,6 +486,7 @@ const schema = compileSchema( {
486
486
  val: {
487
487
  type: value,
488
488
  inKind: [ '$column', 'enum' ],
489
+ xorException: '#', // see xorGroup :expr
489
490
  // see also extra handling for 'element' in extension, see definition()
490
491
  },
491
492
  literal: {
@@ -499,6 +500,7 @@ const schema = compileSchema( {
499
500
  // Note: We emit a warning if '#' is used in enums. Because the compiler
500
501
  // can generate CSN like this, we need to be able to parse it.
501
502
  inKind: [ '$column', 'enum' ],
503
+ xorException: 'val', // see xorGroup :expr
502
504
  // see also extra handling for 'element' in extension, see definition()
503
505
  },
504
506
  path: { // in CSN v0.1.0 'foreignKeys'
@@ -1132,7 +1132,7 @@ function value( node ) {
1132
1132
  return extra( { '=': node.variant ? `${ ref }#${ pathName(node.variant.path) }` : ref }, node );
1133
1133
  }
1134
1134
  if (node.literal === 'enum')
1135
- return extra( { '#': node.sym.id }, node );
1135
+ return enumValue( node );
1136
1136
  if (node.literal === 'array')
1137
1137
  return node.val.map( value );
1138
1138
  if (node.literal === 'token' && node.val === '...')
@@ -1146,6 +1146,15 @@ function value( node ) {
1146
1146
  return r;
1147
1147
  }
1148
1148
 
1149
+ function enumValue( node ) {
1150
+ const r = extra( { '#': node.sym.id }, node );
1151
+ const sym = node.sym._artifact;
1152
+ // add calculated `val`, but not for chained symbols:
1153
+ if (sym && (!gensrcFlavor || gensrcFlavor === 'column') && !sym.value?.sym)
1154
+ r.val = sym.value ? sym.value.val : sym.name.id;
1155
+ return r;
1156
+ }
1157
+
1149
1158
  function targetElement( val, csn, node ) {
1150
1159
  const key = addExplicitAs( { ref: val.path.map( pathItem ) },
1151
1160
  node.name, neqPath( val ) );
@@ -1210,10 +1219,10 @@ function exprInternal( node, xprParens ) {
1210
1219
  return extra( { ref }, node );
1211
1220
  }
1212
1221
  if (node.literal) {
1213
- if (typeof node.val === node.literal || node.val === null)
1222
+ if (node.literal === 'enum')
1223
+ return enumValue( node );
1224
+ else if (typeof node.val === node.literal || node.val === null)
1214
1225
  return extra( { val: node.val }, node );
1215
- else if (node.literal === 'enum')
1216
- return extra( { '#': node.sym.id }, node );
1217
1226
  else if (node.literal === 'token')
1218
1227
  return node.val; // * in COUNT(*)
1219
1228
  return extra( { val: node.val, literal: node.literal }, node );
@@ -7,7 +7,6 @@ const {
7
7
  } = require('./textUtils');
8
8
 
9
9
  const fencedCommentRegEx = /^\s*[*]/;
10
- const footerFenceRegEx = /\s*[*]+\/$/;
11
10
  const hasContentOnFirstLineRegEx = /\/\*+\s*\S/;
12
11
 
13
12
  /**
@@ -37,9 +36,8 @@ function parseDocComment( comment ) {
37
36
  if (lines.length === 1) {
38
37
  // Special case for one-liners.
39
38
  // Remove "/***/" and trim white space and asterisks.
40
- const content = lines[0]
41
- .replace(/^\/[*]{2,}/, '')
42
- .replace(/\**\/$/, '') // for `/*****/`, only `/` remains
39
+ let content = lines[0].replace(/^\/[*]{2,}/, '');
40
+ content = removeFooterFence(content) // for `/*****/`, only `/` remains
43
41
  .replace('*\\/', '*/') // escape sequence
44
42
  .trim();
45
43
  return isWhitespaceOrNewLineOnly(content) ? null : content;
@@ -153,7 +151,15 @@ function removeHeaderFence( line ) {
153
151
  * @returns {string} header without fence
154
152
  */
155
153
  function removeFooterFence( line ) {
156
- return line.replace(footerFenceRegEx, '');
154
+ let trimAt = line.length - 1;
155
+ // '-1': remove trailing `/`
156
+ for (let i = trimAt - 1; i >= 0 && line[i] === '*'; --i)
157
+ trimAt = i;
158
+ // We know that trimAt is at a '*', regardless of whether the previous loop ran.
159
+ for (let i = trimAt - 1; i >= 0 && /^\s$/.test(line[i]); --i)
160
+ trimAt = i;
161
+ // Either trimAt is a ' ' or '*', regardless of whether any loop ran.
162
+ return line.slice(0, trimAt);
157
163
  }
158
164
 
159
165
  /**
@@ -235,9 +235,9 @@ function singleTokenDeletion( recognizer ) {
235
235
  return null;
236
236
 
237
237
  const nextTokenType = recognizer.getTokenStream().LA(2);
238
- const { Number } = recognizer.constructor;
239
- if (nextTokenType > Number && // next token is Id|Unreserved|IllegalToken
240
- token.type <= Number) // current token is not
238
+ const { Number: num } = recognizer.constructor;
239
+ if (nextTokenType > num && // next token is Id|Unreserved|IllegalToken
240
+ token.type <= num) // current token is not
241
241
  return null;
242
242
 
243
243
  const expecting = this.getExpectedTokens(recognizer);
@@ -1059,6 +1059,8 @@ function assignAnnotationValue( anno, value ) {
1059
1059
  }
1060
1060
 
1061
1061
  function relevantDigits( val ) {
1062
+ // We know the value does not contain newlines, hence the RegEx is safe.
1063
+ // eslint-disable-next-line sonarjs/slow-regex
1062
1064
  val = val.replace( /e.+$/i, '' );
1063
1065
 
1064
1066
  // To avoid the super-linear RegEx `0+$`, use the non-backtracking version and
@@ -1014,7 +1014,12 @@ function getRootArtifactName( artifactName, csn ) {
1014
1014
  // 'foo' => 'foo';
1015
1015
  // 'foo::bar' => 'bar'
1016
1016
  function getLastPartOf( name ) {
1017
- return name.substring(name.search(/[^.:]+$/));
1017
+ // Not using RegEx /[^.:]+$/ to avoid ReDoS.
1018
+ for (let i = name.length - 1; i >= 0; --i) {
1019
+ if (name[i] === '.' || name[i] === ':')
1020
+ return name.substring(i + 1);
1021
+ }
1022
+ return name;
1018
1023
  }
1019
1024
 
1020
1025
  // Return the last part of reference array 'ref'
@@ -197,6 +197,7 @@ optionProcessor.command('H, toHana')
197
197
  .option(' --assert-integrity-type <type>', { valid: ['RT', 'DB'], ignoreCase: true })
198
198
  .option(' --pre2134ReferentialConstraintNames')
199
199
  .option(' --disable-hana-comments')
200
+ .option(' --standard-database-functions')
200
201
  .help(`
201
202
  Usage: cdsc toHana [options] <files...>
202
203
 
@@ -233,6 +234,7 @@ optionProcessor.command('H, toHana')
233
234
  DB : Create database constraints for associations
234
235
  --pre2134ReferentialConstraintNames Do not prefix the constraint identifier with "c__"
235
236
  --disable-hana-comments Disable rendering of doc comments as SAP HANA comments.
237
+ --standard-database-functions Enable rendering of standard database function mappings.
236
238
  `);
237
239
 
238
240
  optionProcessor.command('O, toOdata')
@@ -340,6 +342,7 @@ optionProcessor.command('Q, toSql')
340
342
  .option(' --better-sqlite-session-variables <bool>')
341
343
  .option(' --transitive-localized-views')
342
344
  .option(' --with-hana-associations <bool>', { valid: [ 'true', 'false' ] })
345
+ .option(' --standard-database-functions')
343
346
  .help(`
344
347
  Usage: cdsc toSql [options] <files...>
345
348
 
@@ -399,9 +402,10 @@ optionProcessor.command('Q, toSql')
399
402
  --transitive-localized-views If set, the backends will create localized convenience views for
400
403
  those views, that only have an association to a localized entity/view.
401
404
  --with-hana-associations <bool>
402
- Enable and disable rendering of "WITH ASSOCIATIONS" for sqlDialect 'hana'.
405
+ Enable or disable rendering of "WITH ASSOCIATIONS" for sqlDialect 'hana'.
403
406
  true : (default) Render "WITH ASSOCIATIONS"
404
407
  false : Do not render "WITH ASSOCIATIONS"
408
+ --standard-database-functions Enable rendering of standard database function mappings.
405
409
  `);
406
410
 
407
411
  optionProcessor.command('toRename')
@@ -9,6 +9,7 @@ const { functionsWithoutParentheses } = require('./identifiers');
9
9
  const { pathName } = require('../compiler/utils');
10
10
  const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
11
11
  const parserTokens = { // TODO: precompile into specialFunction
12
+ __proto__: null,
12
13
  GenericIntro: 'intro',
13
14
  GenericExpr: 'expr',
14
15
  GenericSeparator: 'separator',
@@ -36,11 +37,18 @@ const queryOps = {
36
37
  minus: 'query',
37
38
  };
38
39
 
40
+ const extensionsCode = {
41
+ __proto__: null,
42
+ definitions: 'extend … with definitions',
43
+ context: 'extend context',
44
+ service: 'extend service',
45
+ };
46
+
39
47
  const PRECEDENCE_OF_IN_PREDICATE = 10;
40
48
  const PRECEDENCE_OF_EQUAL = 10;
41
49
 
42
50
  class AstBuildingParser extends BaseParser {
43
- leanConditions = { afterBrace: true };
51
+ leanConditions = { afterBrace: true, fail: true };
44
52
 
45
53
  constructor( lexer, keywords, table, options, messageFunctions ) {
46
54
  super( lexer, keywords, table ); // lexer has file
@@ -86,12 +94,18 @@ class AstBuildingParser extends BaseParser {
86
94
 
87
95
  reportUnexpectedToken_() {
88
96
  const token = this.la();
89
- const expecting = this.expectingArray();
90
- const err = this.error( 'syntax-unexpected-token', token,
91
- { offending: this.antlrName( token ), expecting } );
97
+ const args = { offending: this.antlrName( token ), expecting: this.expectingArray() };
98
+ const errorMethod = this.conditionTokenIdx === this.tokenIdx &&
99
+ this[`${ this.conditionName }Error`];
100
+ let err = errorMethod && errorMethod.call( this, args, token );
101
+ // TODO: should we set the msg variant always? (→ no nestedExpandError necessary)
102
+ if (errorMethod && !err)
103
+ args['#'] ??= this.conditionName;
104
+ err ||= this.error( 'syntax-unexpected-token', token, args );
92
105
  // No 'unwanted' variant, no 'syntax-missing-token'
93
- err.expectedTokens = expecting;
106
+ err.expectedTokens = args.expecting;
94
107
  }
108
+
95
109
  reportReservedWord_() {
96
110
  const token = this.la();
97
111
  const err = this.message( 'syntax-unexpected-reserved-word', token,
@@ -100,13 +114,13 @@ class AstBuildingParser extends BaseParser {
100
114
  err.expectedTokens = this.expectingArray();
101
115
  }
102
116
 
103
- tableWithoutAs() {
117
+ tableWithoutAs() { // not used in <guard=…>, only called by other guard
104
118
  // TODO TOOL: if the tool properly creates `default: this.giR()`, this
105
119
  // condition method is most likely not necessary
106
120
  const { keyword } = this.la();
107
121
  // TODO: if necessary, we could allow some keywords, and just make sure that
108
122
  // all JOIN variants are still possible
109
- return !keyword || this.keywords[keyword] == null;
123
+ return keyword && this.keywords[keyword] != null;
110
124
  }
111
125
 
112
126
  /**
@@ -116,22 +130,22 @@ class AstBuildingParser extends BaseParser {
116
130
  * recursive call, it can finally turn to be both a query or expr/table
117
131
  * - <prepare=queryOnLeft, arg=‹SomeVal›>: make the current parentheses
118
132
  * context to be not a query anymore
119
- * - <cond=queryOnLeft> tests whether the expression on the left is a query
120
- * - <cond=queryOnLeft, arg=‹SomeVal›>: tests whether the expression on the
133
+ * - <guard=queryOnLeft> tests whether the expression on the left is a query
134
+ * - <guard=queryOnLeft, arg=‹SomeVal›>: tests whether the expression on the
121
135
  * left is a query, then make the current context to be not a query anymore
122
- * - <cond=queryOnLeft, arg=tableWithoutAs>: …after having checked
136
+ * - <guard=queryOnLeft, arg=tableWithoutAs>: …after having checked
123
137
  * whether the next token is no (reserved or unreserved) keyword
124
138
  */
125
139
  queryOnLeft( test, arg ) {
126
140
  if (arg === 'tableWithoutAs') {
127
- if (!this.tableWithoutAs())
128
- return false;
141
+ if (this.tableWithoutAs())
142
+ return true;
129
143
  }
130
144
  else if (!arg && !test) {
131
145
  // provide new dynamic parentheses context, except with direct
132
146
  // recursive call:
133
147
  if (this.inSameRule_( this.s, this.stack.at( -1 ).followState ))
134
- return true;
148
+ return false;
135
149
  this.dynamic_.parenthesesCtx = [ null ];
136
150
  this._tracePush( 'Parentheses()' );
137
151
  }
@@ -139,7 +153,7 @@ class AstBuildingParser extends BaseParser {
139
153
  const noQuery = parenthesesCtx?.[0];
140
154
  if (arg && parenthesesCtx)
141
155
  parenthesesCtx[0] = arg;
142
- return !noQuery;
156
+ return noQuery;
143
157
  }
144
158
 
145
159
  prepareSpecialFunction() {
@@ -187,14 +201,21 @@ class AstBuildingParser extends BaseParser {
187
201
  return (generic === 'separator') ? 'GenericSeparator' : ',';
188
202
  }
189
203
 
190
- translateParserToken_( tokenName ) {
191
- const realTokens = this.dynamic_.generic?.[parserTokens[tokenName]];
192
- // TODO: avoid parserTokens dict, use lower-case in specialFunctions
193
- return realTokens?.map( s => s.toLowerCase() ) ?? [ tokenName ];
204
+ addTokenToSet_( set, tokenName, val, collectKeywordsOnly ) {
205
+ const realTokens = parserTokens[tokenName] && this.dynamic_.generic?.[parserTokens[tokenName]];
206
+ // TODO: avoid 2nd parserTokens dict use, use lower-case in specialFunctions
207
+ if (!realTokens) {
208
+ super.addTokenToSet_( set, tokenName, val, collectKeywordsOnly );
209
+ }
210
+ else {
211
+ for (const t of realTokens)
212
+ super.addTokenToSet_( set, t.toLowerCase(), val, collectKeywordsOnly );
213
+ }
194
214
  }
195
215
 
196
216
  inSelectItem( _test, arg ) { // only as action
197
- this.dynamic_.inSelectItem = arg;
217
+ this.dynamic_.inSelectItem = arg ||
218
+ (this.tokens[this.tokenIdx - 2].type === '.' ? 'inline' : 'expand');
198
219
  }
199
220
 
200
221
  /**
@@ -202,38 +223,47 @@ class AstBuildingParser extends BaseParser {
202
223
  * (also inside sub queries in those, which will be rejected later anyway)
203
224
  */
204
225
  modifierRestriction() {
205
- return this.dynamic_.inSelectItem !== 'nested';
226
+ const { inSelectItem } = this.dynamic_;
227
+ // TODO: really reject for top-level "inline"?
228
+ return inSelectItem === 'expand' || inSelectItem === 'inline';
229
+ }
230
+ modifierRestrictionError( args, offending ) {
231
+ return this.error( 'syntax-unexpected-modifier', offending, args,
232
+ // TODO: we would have text variant for expand or inline,
233
+ // but we probably allow `key` in nested top-level inline
234
+ 'Unexpected $(OFFENDING) in nested expand/inline, expecting $(EXPECTING)' );
206
235
  }
207
236
 
208
237
  isDotForPath() { // see also inSelectItem
238
+ // TODO: also consider whether we are in the <prefer>ed `valuePath` branch
209
239
  if (this.dynamic_.inSelectItem == null)
210
- return true;
240
+ return false;
211
241
  // TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
212
242
  // (as <prepare>)
213
243
  const next = this.tokens[this.tokenIdx + 1]?.type;
214
- return next !== '*' && next !== '{';
244
+ return next === '*' || next === '{';
215
245
  }
216
246
 
217
- notAfterEntityArgOrFilter( mode ) {
247
+ notAfterEntityArgOrFilter( mode ) { // TODO: for <hide>
218
248
  if (mode !== 'M')
219
- return true;
249
+ return false;
220
250
  const { type } = this.lb();
221
251
  if (type !== ')' && type !== ']')
222
- return true;
252
+ return false;
223
253
  const { followState } = this.stack.at( -1 );
224
- return !this.table[followState][':'];
254
+ return this.table[followState][':'];
225
255
  }
226
256
 
227
257
  // <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
228
258
  // completion for `… default 3 not ~;` → currently just `null` but hey
229
259
  isNegatedRelation( _test, prec ) {
230
- return this.tokens[this.tokenIdx + 1]?.keyword !== 'null' &&
260
+ return this.tokens[this.tokenIdx + 1]?.keyword === 'null' ||
231
261
  this.precNone_( _test, prec );
232
262
  }
233
263
 
234
264
  isNamedArg() {
235
- const { type } = this.tokens[this.tokenIdx + 1];
236
- return type === ':' || type === '=>';
265
+ const type = this.tokens[this.tokenIdx + 1]?.type;
266
+ return type !== ':' && type !== '=>';
237
267
  }
238
268
 
239
269
  /**
@@ -241,45 +271,66 @@ class AstBuildingParser extends BaseParser {
241
271
  * `namespace`
242
272
  */
243
273
  namespaceRestriction() {
244
- return ++this.topLevel$ < 1;
274
+ return ++this.topLevel$ > 0;
245
275
  }
246
276
 
247
277
  /**
248
278
  * `extend`/`annotate` is forbidden inside `extend … with definitions` and
249
- * variants
279
+ * variants. TODO: combine with `vocabularyRestriction`.
250
280
  */
251
281
  extensionRestriction() {
252
- // TODO: use `syntax-unexpected-extension` as message
282
+ // 'syntax-unexpected-extension': 'Unexpected $(KEYWORD) inside $(CODE) block',
253
283
  const r = this.dynamic_.inExtension;
254
- this.dynamic_.inExtension = true;
255
- return !r;
284
+ this.dynamic_.inExtension = this.tokenIdx + 1;
285
+ return r;
286
+ }
287
+ extensionRestrictionError( args, token ) {
288
+ const extendIdx = this.conditionFailure;
289
+ const variant = this.tokens[extendIdx + 1]?.type === 'Id' &&
290
+ this.tokens[extendIdx].keyword;
291
+ args.code = extensionsCode[variant] || extensionsCode.definitions;
292
+ args['#'] = 'new-parser';
293
+ return this.error( 'syntax-unexpected-extension', token, args );
256
294
  }
257
295
 
258
296
  /**
259
- * `annotation` def is only allowed top-level
297
+ * `annotation` def is only allowed top-level. TODO: combine with `extensionRestriction`
260
298
  */
261
299
  vocabularyRestriction( test ) {
262
- // TODO: use `syntax-unexpected-vocabulary` as message
263
300
  if (!test)
264
- this.dynamic_.inBlock = true;
265
- return !this.dynamic_.inBlock;
301
+ this.dynamic_.inBlock = this.tokenIdx;
302
+ return this.dynamic_.inBlock ?? this.dynamic_.inExtension;
303
+ }
304
+ vocabularyRestrictionError( args, token ) {
305
+ const extendIdx = this.conditionFailure;
306
+ args['#'] = `${ this.tokens[extendIdx - 1].keyword }-new`;
307
+ return this.error( 'syntax-unexpected-vocabulary', token, args );
266
308
  }
267
309
 
268
310
  /**
269
311
  * Restrictions according to the expression of a select column.
270
312
  * Currently only to restrict it to a single `Id` for published associations.
313
+ * No extra syntax-unexpected-assoc for failure.
271
314
  */
272
315
  columnExpr( mode, arg ) {
273
316
  if (mode)
274
- return this.columnExpr$;
275
- // TODO: should we use (text of) syntax-unexpected-assoc somewhere ?
317
+ return !this.columnExpr$;
276
318
  if (arg)
277
319
  this.columnExpr$ = this.tokenIdx;
278
320
  else if (this.columnExpr$ !== this.tokenIdx - 1 ||
279
321
  this.lb().type !== 'Id' ||
280
322
  [ 'true', 'false', 'null' ].includes( this.lb().keyword ) )
281
323
  this.columnExpr$ = null;
282
- return null;
324
+ return true;
325
+ }
326
+
327
+ nestedExpand( mode ) {
328
+ if (!mode)
329
+ this.nestedExpand$ = this.tokenIdx;
330
+ return this.nestedExpand$ !== this.tokenIdx;
331
+ }
332
+ nestedExpandError() {
333
+ // This is intentionally left empty
283
334
  }
284
335
 
285
336
  /**
@@ -299,7 +350,7 @@ class AstBuildingParser extends BaseParser {
299
350
  * now disallow annotation assignments after `= calcExpr`,
300
351
  * ignore doc comment after having called `typeExpression`
301
352
  *
302
- * Called as <cond=…>:
353
+ * Called as <guard=…>:
303
354
  *
304
355
  * - <…, arg=default> in `typeExpression` and `typeProperties`
305
356
  * is `default` allowed? If used, disallow calc and further DEFAULT
@@ -321,14 +372,14 @@ class AstBuildingParser extends BaseParser {
321
372
  let { elementCtx } = this.dynamic_;
322
373
  if (test) {
323
374
  if (elementCtx?.[0] === arg)
324
- return false;
375
+ return arg;
325
376
  if (!elementCtx) { // with type, param, or annotation defs
326
377
  // eslint-disable-next-line no-multi-assign
327
378
  elementCtx = this.dynamic_.elementCtx = [ null, false, false ];
328
379
  }
329
380
  if (arg === 'default') {
330
381
  if (elementCtx[1])
331
- return false;
382
+ return true;
332
383
  elementCtx[1] = true;
333
384
  elementCtx[0] = 'calc';
334
385
  this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
@@ -336,7 +387,7 @@ class AstBuildingParser extends BaseParser {
336
387
  else if (arg === 'notNull') {
337
388
  if (elementCtx[2]) {
338
389
  if (this.la().keyword !== elementCtx[2] || test === 'M') // TODO v6: always error
339
- return false; // error if different nullibility specification
390
+ return true; // error if different nullibility specification
340
391
  }
341
392
  elementCtx[2] = this.la().keyword;
342
393
  }
@@ -347,17 +398,34 @@ class AstBuildingParser extends BaseParser {
347
398
  else if (elementCtx) {
348
399
  elementCtx[0] = arg;
349
400
  }
350
- return true;
401
+ return false;
402
+ }
403
+ elementRestrictionError( args, token ) {
404
+ if (this.conditionFailure !== 'calc')
405
+ return null;
406
+ args.keyword = 'default';
407
+ // TODO: investigate why 'null', '@' are not in the expected-set
408
+ // TODO: simplified version for predictions, such that ops are in expected-set ?
409
+ // TODO: also test `default 3 null = 4`
410
+ return this.error( 'syntax-unexpected-calc', token, args,
411
+ 'Unexpected $(OFFENDING) after $(KEYWORD) clause, expecting $(EXPECTING)' );
351
412
  }
352
413
 
353
414
  noRepeatedCardinality( mode ) {
354
415
  if (this.tokens[this.tokenIdx - 2]?.type !== ']')
355
- return true;
356
- if (mode === 'M')
357
416
  return false;
417
+ if (mode === 'M')
418
+ return true;
358
419
  // currently just warning if same cardinality provided twice
359
420
  const same = { one: '1', many: '*' }[this.la().keyword];
360
- return this.tokens[this.tokenIdx - 3]?.text === same;
421
+ return this.tokens[this.tokenIdx - 3]?.text !== same;
422
+ }
423
+ noRepeatedCardinalityError( args ) {
424
+ let openIdx = this.tokenIdx - 2;
425
+ while (this.tokens[--openIdx].type !== '[')
426
+ ;
427
+ args.location = this.tokens[openIdx].location;
428
+ args.code = '[…]';
361
429
  }
362
430
 
363
431
  /**
@@ -366,21 +434,25 @@ class AstBuildingParser extends BaseParser {
366
434
  *
367
435
  * Beware: mentioned in leanConditions, i.e. executed in predictions!
368
436
  */
369
- afterBrace( test ) {
370
- if (!test)
371
- this.afterBrace$ = this.tokenIdx;
437
+ afterBrace( test, arg ) {
438
+ if (!test) {
439
+ this.afterBrace$
440
+ = (!arg || this.afterBrace$ > 0 && this.tokens[this.afterBrace$ - 1].keyword === arg)
441
+ ? this.tokenIdx
442
+ : -1;
443
+ }
372
444
  // TODO TOOL: the following test belongs to the BaseParser.js:
373
445
  if (this.conditionTokenIdx === this.tokenIdx && // tested on same
374
446
  this.conditionStackLength == null && // after error recover
375
447
  test !== 'M')
376
- return true;
448
+ return false;
377
449
  // Strange optional `;` after PROJECTION ON source: the rule exit prediction
378
450
  // for fromRefWithOptAlias etc now checks C(afterBrace):
379
451
  if (test === 'E' && this.afterBrace$ > 0 &&
380
452
  this.tokens[this.afterBrace$ - 1]?.keyword === 'projection' &&
381
453
  this.tokens[this.afterBrace$].keyword === 'on')
382
- return true;
383
- return this.afterBrace$ === this.tokenIdx;
454
+ return false;
455
+ return this.afterBrace$ !== this.tokenIdx;
384
456
  }
385
457
 
386
458
  /**
@@ -389,8 +461,8 @@ class AstBuildingParser extends BaseParser {
389
461
  annoInSameLine( test ) {
390
462
  if (!test)
391
463
  this.dynamic_.safeAnno = true;
392
- return this.dynamic_.safeAnno ||
393
- this.lb().location.line === this.la().location.line;
464
+ return !this.dynamic_.safeAnno &&
465
+ this.lb().location.line !== this.la().location.line;
394
466
  }
395
467
 
396
468
  /**
@@ -405,21 +477,39 @@ class AstBuildingParser extends BaseParser {
405
477
  else if (arg === 'ellipsis') { // on '...'
406
478
  const { arrayAnno } = this.dynamic_;
407
479
  if (!arrayAnno[0])
408
- return false;
480
+ return arrayAnno[0] == null ? 'duplicate' : arg;
409
481
  arrayAnno[0] = this.tokens[this.tokenIdx + 1]?.keyword;
410
482
  }
411
- else if (arg === 'bracket') {
483
+ else if (arg === 'bracket') { // syntax-invalid-ellipsis
412
484
  // closing bracket not allowed if last `...` in array is with `up to
413
- return typeof this.dynamic_.arrayAnno[0] !== 'string';
485
+ return typeof this.dynamic_.arrayAnno[0] === 'string' && arg;
414
486
  }
415
- else { // orNotEmpty
416
- return this.dynamic_.arrayAnno || this.lb().type !== '{';
487
+ else { // orNotEmpty -> anno value must not be empty struct
488
+ return !this.dynamic_.arrayAnno && this.lb().type === '{' && 'empty';
417
489
  }
418
- return true;
490
+ return false;
491
+ }
492
+ arrayAnnoError( args, token ) {
493
+ if (this.conditionFailure === 'duplicate') {
494
+ args['#'] = 'std'; // normal syntax-unexpected-token
495
+ return null;
496
+ }
497
+ args.code = '...';
498
+ args['#'] = this.conditionFailure;
499
+ return this.error( 'syntax-invalid-anno', token, args );
419
500
  }
420
501
 
421
502
  beforeColon() {
422
- return this.tokens[this.tokenIdx + 1]?.text === ':';
503
+ return this.tokens[this.tokenIdx + 1]?.text !== ':';
504
+ }
505
+
506
+ fail( mode ) {
507
+ // TODO TOOL: the following test belongs to the BaseParser.js:
508
+ if (this.conditionTokenIdx === this.tokenIdx && // tested on same
509
+ this.conditionStackLength == null && // after error recover
510
+ mode !== 'M')
511
+ return false;
512
+ return true; // mode !== 'Y';
423
513
  }
424
514
 
425
515
  // Space handling etc, locations ----------------------------------------------
@@ -635,12 +725,9 @@ class AstBuildingParser extends BaseParser {
635
725
  const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
636
726
  const token = this.prevTokenWithIndex( tokenIndex ) ?? this.tokens[this.tokenIdx - 1];
637
727
  const { parsedAs } = token;
638
- if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword') {
728
+ if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword')
639
729
  token.parsedAs = category;
640
- return { token, parsedAs };
641
- }
642
730
  }
643
- return null;
644
731
  }
645
732
 
646
733
  taggedIfQuery( query ) {
@@ -898,7 +985,7 @@ class AstBuildingParser extends BaseParser {
898
985
  return target;
899
986
  }
900
987
 
901
- // TODO: as condition
988
+ // see also <guard=nestedExpand>
902
989
  reportExpandInline( column, isInline ) {
903
990
  // called before matching `{`
904
991
  if (column.value && !column.value.path) {
@@ -1129,7 +1216,7 @@ class AstBuildingParser extends BaseParser {
1129
1216
  };
1130
1217
  }
1131
1218
 
1132
- // TODO: as condition
1219
+ // no extra message syntax-unexpected-assoc for guard failure
1133
1220
  associationInSelectItem( art ) {
1134
1221
  if (art.name)
1135
1222
  return;
@@ -1348,15 +1435,16 @@ function addOneForDefinition( count, ext ) {
1348
1435
 
1349
1436
  // Significant digits (before exponent) without leading and trailing zeros
1350
1437
  function relevantDigits( val ) {
1438
+ // eslint-disable-next-line sonarjs/slow-regex
1439
+ val = val.replace( /e.+$/i, '' ); // this regex has no newlines -> is not slow
1440
+
1351
1441
  const init = /^[-+0.]+/g; // global flag to have lastIndex
1352
1442
  const zeros = /[0.]+/g;
1353
1443
  if (init.test( val )) // sets init.lastIndex
1354
1444
  zeros.lastIndex = init.lastIndex;
1355
1445
 
1356
1446
  let r;
1357
- while ((r = zeros.exec( val )) != null &&
1358
- zeros.lastIndex < val.length &&
1359
- val.charAt( zeros.lastIndex ).toLowerCase() !== 'e')
1447
+ while ((r = zeros.exec( val )) != null && zeros.lastIndex < val.length)
1360
1448
  ;
1361
1449
  return val.slice( init.lastIndex, r?.index ).replace( /\./, '' );
1362
1450
  }