@sap/cds-compiler 5.4.4 → 5.5.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 (40) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/bin/cds_remove_invalid_whitespace.js +4 -4
  3. package/bin/cds_update_annotations.js +3 -3
  4. package/bin/cds_update_identifiers.js +3 -3
  5. package/lib/api/main.js +18 -30
  6. package/lib/api/validate.js +6 -1
  7. package/lib/base/lazyload.js +28 -0
  8. package/lib/base/location.js +1 -0
  9. package/lib/base/message-registry.js +47 -11
  10. package/lib/base/messages.js +17 -3
  11. package/lib/checks/cdsMap.js +27 -0
  12. package/lib/checks/{dbFeatureFlags.js → featureFlags.js} +1 -1
  13. package/lib/checks/parameters.js +61 -4
  14. package/lib/checks/validator.js +17 -7
  15. package/lib/compiler/define.js +1 -0
  16. package/lib/compiler/index.js +7 -7
  17. package/lib/gen/BaseParser.js +345 -235
  18. package/lib/gen/CdlParser.js +4438 -4492
  19. package/lib/gen/Dictionary.json +2 -2
  20. package/lib/language/antlrParser.js +2 -111
  21. package/lib/main.js +16 -37
  22. package/lib/model/cloneCsn.js +1 -5
  23. package/lib/modelCompare/utils/filter.js +47 -21
  24. package/lib/parsers/AstBuildingParser.js +92 -73
  25. package/lib/parsers/CdlGrammar.g4 +110 -137
  26. package/lib/parsers/index.js +123 -0
  27. package/lib/render/toSql.js +8 -2
  28. package/lib/render/utils/delta.js +33 -1
  29. package/lib/transform/db/{transformExists.js → assocsToQueries/transformExists.js} +12 -407
  30. package/lib/transform/db/assocsToQueries/utils.js +440 -0
  31. package/lib/transform/db/expansion.js +2 -2
  32. package/lib/transform/draft/db.js +14 -3
  33. package/lib/transform/effective/annotations.js +3 -3
  34. package/lib/transform/effective/main.js +5 -7
  35. package/lib/transform/featureFlags.js +5 -0
  36. package/lib/transform/forRelationalDB.js +125 -192
  37. package/lib/transform/transformUtils.js +0 -51
  38. package/lib/utils/objectUtils.js +13 -0
  39. package/package.json +2 -2
  40. package/lib/transform/db/featureFlags.js +0 -5
@@ -70,37 +70,34 @@ class AstBuildingParser extends BaseParser {
70
70
  return this.$messageFunctions.info( id, location?.location || location, args, text );
71
71
  }
72
72
 
73
- expectingArray( token ) {
74
- const expecting = this._expecting( token );
75
- let array = Object.keys( expecting );
76
- // compatibility: replace true+false by Boolean
77
- if (expecting.true && expecting.false)
73
+ expectingArray() {
74
+ const savedState = this.s;
75
+ this.s = this.errorState;
76
+ let array = this.expectingArray_();
77
+ this.s = savedState;
78
+ // compatibility: replace true+false by Boolean - TODO: delete
79
+ if (array.includes( 'true' ))
78
80
  array = [ 'Boolean', ...array.filter( n => n !== 'true' && n !== 'false' ) ];
79
- return array.map( antlrName )
81
+ return array.map( tok => this.antlrName( tok ) )
80
82
  .sort( (a, b) => (tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1) );
81
83
  }
82
84
 
83
- reportUnexpectedToken_( token ) {
84
- const expecting = this.expectingArray( token );
85
+ reportUnexpectedToken_() {
86
+ const token = this.la();
87
+ const expecting = this.expectingArray();
85
88
  const err = this.error( 'syntax-unexpected-token', token,
86
- { offending: antlrName( token ), expecting } );
89
+ { offending: this.antlrName( token ), expecting } );
87
90
  // No 'unwanted' variant, no 'syntax-missing-token'
88
91
  err.expectedTokens = expecting;
89
92
  }
90
- reportReservedWord_( token ) {
93
+ reportReservedWord_() {
94
+ const token = this.la();
91
95
  const err = this.message( 'syntax-unexpected-reserved-word', token,
92
96
  { code: token.text, delimited: token.text } );
93
97
  // TODO: at least if one expected keyword is similar, mention expected set
94
98
  err.expectedTokens = this.expectingArray();
95
99
  }
96
100
 
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' );
102
- }
103
-
104
101
  tableWithoutAs() {
105
102
  // TODO TOOL: if the tool properly creates `default: this.giR()`, this
106
103
  // condition method is most likely not necessary
@@ -180,7 +177,7 @@ class AstBuildingParser extends BaseParser {
180
177
  return this.lGenericIntroOrExpr( false );
181
178
  }
182
179
 
183
- lGenericSeparator() {
180
+ lGenericSeparator() { // TODO: { keyword, type } as arg ?
184
181
  const { keyword, type } = this.la();
185
182
  // TODO: use lower-case in specialFunctions
186
183
  const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
@@ -191,7 +188,7 @@ class AstBuildingParser extends BaseParser {
191
188
  translateParserToken_( tokenName ) {
192
189
  const realTokens = this.dynamic_.generic?.[parserTokens[tokenName]];
193
190
  // TODO: avoid parserTokens dict, use lower-case in specialFunctions
194
- return realTokens?.map( s => s.toLowerCase() ) ?? [];
191
+ return realTokens?.map( s => s.toLowerCase() ) ?? [ tokenName ];
195
192
  }
196
193
 
197
194
  inSelectItem( _test, arg ) { // only as action
@@ -211,14 +208,14 @@ class AstBuildingParser extends BaseParser {
211
208
  return true;
212
209
  // TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
213
210
  // (as <prepare>)
214
- const next = this.tokens[this.tokenIdx + 1].type;
211
+ const next = this.tokens[this.tokenIdx + 1]?.type;
215
212
  return next !== '*' && next !== '{';
216
213
  }
217
214
 
218
215
  // <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
219
216
  // completion for `… default 3 not ~;` → currently just `null` but hey
220
217
  isNegatedRelation( _test, prec ) {
221
- return this.tokens[this.tokenIdx + 1].keyword !== 'null' &&
218
+ return this.tokens[this.tokenIdx + 1]?.keyword !== 'null' &&
222
219
  this.precNone_( _test, prec );
223
220
  }
224
221
 
@@ -261,13 +258,13 @@ class AstBuildingParser extends BaseParser {
261
258
  *
262
259
  * Called as <prepare=…>:
263
260
  *
264
- * - <…, arg=default> in `returnsSpec`: after `returns`
265
- * disallow `default` in `typeExpression`
266
261
  * - <…,arg=elem> in `elementDef` (before calling `typeExpression`):
267
262
  * allow `default`/`= calcExpr` with final annotation assignments,
268
263
  * delay final doc comment
264
+ * - <…, arg=default> in `returnsSpec`: after `returns`
265
+ * disallow `default` in `typeExpression`
269
266
  * - <…, arg=calc> in `typeExpression` (with associations, etc)
270
- * now disallow `= calcExpr` (with annotation assignments) in `elementDef`,
267
+ * now disallow `= calcExpr` in `elementDef`,
271
268
  * do not delay final doc comments anymore
272
269
  * - <…, arg=anno> in `typeExpression` after enums:
273
270
  * now disallow annotation assignments after `= calcExpr`,
@@ -275,25 +272,48 @@ class AstBuildingParser extends BaseParser {
275
272
  *
276
273
  * Called as <cond=…>:
277
274
  *
278
- * - <…, arg=default> in `nullabilityAndDefault`:
279
- * is `default` allowed? If used, disallow calc (with anno assignments)
275
+ * - <…, arg=default> in `typeExpression` and `typeProperties`
276
+ * is `default` allowed? If used, disallow calc and further DEFAULT
277
+ * - <…, arg=notNull> in `typeExpression` and `typeProperties`
278
+ * is `null`/`not null` allowed? ensures that it is only used once
280
279
  * - <…, arg=calc> in `elementDef`:
281
- * is `= calcExpr` allowed? If so, check for final doc
280
+ * is `= calcExpr` allowed? not with struct, assoc or MANY…
282
281
  * - <…, arg=anno> in `elementDef`:
283
- * are annotation assignments after `= calcExpr` allowed?
282
+ * are annotation assignments after `= calcExpr` allowed? not with ENUM…
283
+ *
284
+ * The value of the dynamic var `elementCtx` looks like [REJECTED, DEFAULT,
285
+ * NOTNULL] where
286
+ *
287
+ * - REJECTED is the string containing a to-be-rejected test `arg`
288
+ * - DEFAULT: true if `default` had been provided
289
+ * - NOTNULL: true if `null` or `not null` had been provided
284
290
  */
285
291
  elementRestriction( test, arg ) {
286
- const { elementCtx } = this.dynamic_;
292
+ let { elementCtx } = this.dynamic_;
287
293
  if (test) {
288
- if (!elementCtx || elementCtx[0] === arg)
289
- return !elementCtx;
294
+ if (elementCtx?.[0] === arg)
295
+ return false;
296
+ if (!elementCtx) { // with type, param, or annotation defs
297
+ // eslint-disable-next-line no-multi-assign
298
+ elementCtx = this.dynamic_.elementCtx = [ null, false, false ];
299
+ }
290
300
  if (arg === 'default') {
301
+ if (elementCtx[1])
302
+ return false;
303
+ elementCtx[1] = true;
291
304
  elementCtx[0] = 'calc';
292
305
  this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
293
306
  }
307
+ else if (arg === 'notNull') {
308
+ if (elementCtx[2]) {
309
+ if (this.la().keyword !== elementCtx[2] || test === 'M') // TODO v6: always error
310
+ return false; // error if different nullibility specification
311
+ }
312
+ elementCtx[2] = this.la().keyword;
313
+ }
294
314
  }
295
315
  else if (arg === 'elem' || arg === 'default') {
296
- this.dynamic_.elementCtx = [ arg ];
316
+ this.dynamic_.elementCtx = [ arg, false, false ];
297
317
  }
298
318
  else if (elementCtx) {
299
319
  elementCtx[0] = arg;
@@ -301,10 +321,6 @@ class AstBuildingParser extends BaseParser {
301
321
  return true;
302
322
  }
303
323
 
304
- inExpandInline() { // not as <cond>
305
- this.dynamic_.inSelectItem = 'nested';
306
- }
307
-
308
324
  /**
309
325
  * `;` between statements is optional only after a `}` (ex braces of structure
310
326
  * values for annotations).
@@ -338,8 +354,11 @@ class AstBuildingParser extends BaseParser {
338
354
  const { arrayAnno } = this.dynamic_;
339
355
  if (!arrayAnno[0])
340
356
  return false;
341
- if (this.tokens[this.tokenIdx + 1]?.type === ',')
342
- arrayAnno[0] = false;
357
+ arrayAnno[0] = this.tokens[this.tokenIdx + 1]?.keyword;
358
+ }
359
+ else if (arg === 'bracket') {
360
+ // closing bracket not allowed if last `...` in array is with `up to
361
+ return typeof this.dynamic_.arrayAnno[0] !== 'string';
343
362
  }
344
363
  else { // orNotEmpty
345
364
  return this.dynamic_.arrayAnno || this.lb().type !== '{';
@@ -737,22 +756,10 @@ class AstBuildingParser extends BaseParser {
737
756
  * - misplaced doc comments would lead to a parse error (incompatible),
738
757
  * - would influence the prediction and error recovery,
739
758
  * - is only slightly "more declarative" in the grammar.
740
- *
741
- * With argument `delayed`, potentially delay the doc processing.
742
- * See also `elementRestriction`.
743
759
  */
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
-
760
+ docComment( art ) {
754
761
  const { line: prevLine, col: prevCol } = this.lb()?.location ?? { line: 0, col: 0 };
755
- const { line: currLine, col: currCol } = this.la().location;
762
+ const { line: currLine, col: currCol } = (this.la() ?? this.lb()).location;
756
763
  let token;
757
764
  for (;;) {
758
765
  token = this.docComments[this.docCommentIndex];
@@ -783,16 +790,20 @@ class AstBuildingParser extends BaseParser {
783
790
  }
784
791
  }
785
792
 
793
+ // TODO: can we remove `;`/EOF from the expected-set for `annotate Foo with ⎀`?
786
794
  checkWith( keyword ) {
787
795
  if (this.lb() !== keyword)
788
796
  return;
789
797
  const tok = this.la();
790
- if (this.docCommentIndex < tok.location.tokenIndex &&
791
- this.docCommentIndex > this.lb().location.tokenIndex)
798
+ const docTokenIndex = this.docCommentIndex &&
799
+ this.docComments[this.docCommentIndex - 1].location.tokenIndex;
800
+ if (docTokenIndex < tok.location.tokenIndex &&
801
+ docTokenIndex > this.lb().location.tokenIndex)
792
802
  return;
793
- const expecting = this.expectingArray(); // TODO: filter
803
+ // filter out what comes after current rule (no generic way necessary):
804
+ const expecting = this.expectingArray().filter( t => t !== '<EOF>' && t !== '\'}\'' );
794
805
  const msg = this.warning( 'syntax-unexpected-semicolon', tok,
795
- { offending: antlrName( tok ), expecting, keyword: 'with' },
806
+ { offending: this.antlrName( tok ), expecting, keyword: 'with' },
796
807
  // eslint-disable-next-line @stylistic/js/max-len
797
808
  'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
798
809
  msg.expectedTokens = expecting;
@@ -801,13 +812,16 @@ class AstBuildingParser extends BaseParser {
801
812
  setNullability( art, val, location = this.lb().location ) {
802
813
  const notNull = { val, location };
803
814
  if (art.notNull) {
804
- this.reportDuplicateClause( 'notNull', art.notNull, notNull,
815
+ // complain about the second
816
+ this.reportDuplicateClause( 'notNull', notNull, art.notNull,
805
817
  (val ? 'not null' : 'null') );
806
818
  }
807
- art.notNull = notNull;
819
+ else {
820
+ art.notNull = notNull;
821
+ }
808
822
  }
809
823
 
810
- setAssocAndComposition( art, assoc, card, target ) {
824
+ setAssocAndComposition( art, assoc, card, target = {} ) {
811
825
  const { location } = assoc;
812
826
  art.type = {
813
827
  path: [ { id: keywordTypeNames[assoc.keyword], location } ],
@@ -816,7 +830,7 @@ class AstBuildingParser extends BaseParser {
816
830
  };
817
831
  art.target = target;
818
832
  if (!card)
819
- return;
833
+ return target;
820
834
 
821
835
  const targetMax = (card.keyword === 'one')
822
836
  ? { val: 1, literal: 'number', location: card.location }
@@ -829,17 +843,19 @@ class AstBuildingParser extends BaseParser {
829
843
  else {
830
844
  art.cardinality = { targetMax, location: targetMax.location };
831
845
  }
846
+ return target;
832
847
  }
833
848
 
834
849
  reportExpandInline( column, isInline ) {
850
+ // called before matching `{`
835
851
  const { name } = column;
836
852
  if (column.value && !column.value.path) {
837
- const token = this.la();
838
853
  // improve error location when using "inline" `.{…}` after ref (arguments and
839
854
  // filters not covered, not worth the effort); after an expression where
840
855
  // the last token is an identifier, not the `.` is wrong, but the `{`:
841
- // if (isInline && !name && this._input.LT(-1).type >= this.constructor.Identifier)
842
- // token = this._input.LT(2); -- TODO: still valid?
856
+ const token = (isInline && this.tokens[this.tokenIdx - 2].type !== 'Id')
857
+ ? this.lb()
858
+ : this.la();
843
859
  this.error( 'syntax-unexpected-nested-proj', token,
844
860
  { code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }' },
845
861
  'Unexpected $(CODE); nested projections can only be used after a reference' );
@@ -868,9 +884,10 @@ class AstBuildingParser extends BaseParser {
868
884
  col: chosen.location.col,
869
885
  };
870
886
  if (erroneous.val === chosen.val) {
887
+ // TODO v6: duplicate clause = error, independently whether it is the same
871
888
  this.warning( 'syntax-duplicate-equal-clause', erroneous.location, args );
872
889
  }
873
- else {
890
+ else if (prop !== 'notNull') { // already via guard in grammar
874
891
  if (literalValIfNotEq)
875
892
  args.code = chosen.val;
876
893
  this.message( 'syntax-duplicate-clause', erroneous.location, args );
@@ -1277,6 +1294,17 @@ class AstBuildingParser extends BaseParser {
1277
1294
  parent, 'elements', kind, seg );
1278
1295
  }
1279
1296
  }
1297
+
1298
+ // For compatibility with ANTLR-based parser:
1299
+ antlrName( type ) {
1300
+ if (typeof type !== 'string') {
1301
+ type = (!type.parsedAs && this.keywords[type.keyword ?? ''] != null ||
1302
+ type.parsedAs === 'keyword') && type.keyword || type.type;
1303
+ }
1304
+ if (/^[A-Z]+/.test( type ))// eslint-disable-next-line no-nested-ternary
1305
+ return (type === 'Id') ? 'Identifier' : (type === 'EOF') ? '<EOF>' : type;
1306
+ return (/^[a-z]+/.test( type )) ? type.toUpperCase() : `'${ type }'`;
1307
+ }
1280
1308
  }
1281
1309
 
1282
1310
  function addOneForDefinition( count, ext ) {
@@ -1299,15 +1327,6 @@ function relevantDigits( val ) {
1299
1327
  }
1300
1328
 
1301
1329
 
1302
- // For compatibility with ANTLR-based parser:
1303
- function antlrName( type ) {
1304
- if (typeof type !== 'string')
1305
- type = (!type.parsedAs || type.parsedAs === 'keyword') && type.keyword || type.type;
1306
- if (/^[A-Z]+/.test( type ))// eslint-disable-next-line no-nested-ternary
1307
- return (type === 'Id') ? 'Identifier' : (type === 'EOF') ? '<EOF>' : type;
1308
- return (/^[a-z]+/.test( type )) ? type.toUpperCase() : `'${ type }'`;
1309
- }
1310
-
1311
1330
  // Used for sorting in messages (TODO: make it part of messages.js?)
1312
1331
  const token1sort = {
1313
1332
  // 0: Identifier, Number, ...