@sap/cds-compiler 5.5.2 → 5.6.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@
7
7
  Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
8
8
  The compiler behavior concerning `beta` features can change at any time without notice.
9
9
 
10
+ ## Version 5.6.0 - 2024-12-12
11
+
12
+ ### Added
13
+
14
+ - Allow to refer to draft state element `HasActiveEntity` and `HasDraftEntity` via variable `$draft` in annotation path expressions.
15
+ - for.odata|to.edm(x): Introduce annotating the generated foreign keys
16
+
17
+ ### Changed
18
+
19
+ - Update OData vocabularies: 'Common', 'EntityRelationship', 'UI'
20
+
10
21
  ## Version 5.5.2 - 2024-12-02
11
22
 
12
23
  ### Fixed
@@ -131,7 +142,7 @@ The compiler behavior concerning `beta` features can change at any time without
131
142
  ### Added
132
143
 
133
144
  - cdsc: Option `--stdin` was added to support input via standard input, e.g. `cat file.cds | cdsc --stdin`
134
- - Allow to refer to draft state element `IsActiveEntity` via magic variable `$draft.IsActiveEntity` in annotation path expressions.
145
+ - Allow to refer to draft state element `IsActiveEntity` via variable `$draft.IsActiveEntity` in annotation path expressions.
135
146
  + for.odata: During draft augmentation `$draft.IsActiveEntity` is rewritten to `$self.IsActiveEntity` for all draft enabled
136
147
  entities (root and sub nodes but not for named types or entity parameters).
137
148
  + to.edm(x): (V4 only) Allow to refer to an entity element in a bound action via `$self` and not only via explicit binding parameter
package/bin/cdsse.js CHANGED
@@ -158,6 +158,9 @@ function tokensAt( buf, _offset, col, symbol ) {
158
158
  else if (/^[A-Z_]+$/.test( n )) {
159
159
  console.log( n.toLowerCase(), 'keyword' );
160
160
  }
161
+ else if (n === 'Boolean') {
162
+ console.log( 'true keyword\nfalse keyword' );
163
+ }
161
164
  else if (n !== 'Identifier') {
162
165
  console.log( n, 'unknown' );
163
166
  }
@@ -1271,6 +1271,8 @@ const centralMessageTexts = {
1271
1271
  'magic': 'Unexpected magic variable $(ELEMREF) in $(ANNO)',
1272
1272
  'bparam_v2_expl': 'Unexpected explicit binding parameter path $(ELEMREF) for OData $(VERSION) in $(ANNO)',
1273
1273
  'bparam_v2_impl': 'Unexpected implicit binding parameter path $(ELEMREF) for OData $(VERSION) in $(ANNO)',
1274
+ // forOdata/generateForeignKeys
1275
+ 'fk_substitution': 'Expected foreign key path $(ELEMREF) in $(ANNO) to end in a scalar typed leaf element',
1274
1276
  },
1275
1277
  // -----------------------------------------------------------------------------------
1276
1278
  // OData Message section ends here, no messages below this line
@@ -206,6 +206,8 @@ const magicVariables = {
206
206
  $draft: {
207
207
  elements: {
208
208
  IsActiveEntity: {},
209
+ HasActiveEntity: {},
210
+ HasDraftEntity: {},
209
211
  },
210
212
  // Require that elements are accessed, i.e. no $draft, only $draft.<element>.
211
213
  $requireElementAccess: true,
@@ -18,6 +18,7 @@
18
18
  Common (published)
19
19
  Communication (published)
20
20
  DataIntegration (published)
21
+ EntityRelationship (experimental)
21
22
  Graph (published, experimental)
22
23
  Hierarchy (published, experimental)
23
24
  HTML5 (published, experimental)
@@ -75,6 +76,11 @@ const vocabularyDefinitions = {
75
76
  inc: { Alias: 'DataIntegration', Namespace: 'com.sap.vocabularies.DataIntegration.v1' },
76
77
  int: { filename: 'DataIntegration.xml' },
77
78
  },
79
+ EntityRelationship: {
80
+ ref: { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/EntityRelationship.xml' },
81
+ inc: { Alias: 'EntityRelationship', Namespace: 'com.sap.vocabularies.EntityRelationship.v1' },
82
+ int: { filename: 'EntityRelationship.xml' },
83
+ },
78
84
  Graph: {
79
85
  ref: { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/Graph.xml' },
80
86
  inc: { Alias: 'Graph', Namespace: 'com.sap.vocabularies.Graph.v1' },
@@ -1,4 +1,4 @@
1
- // Base class for generated parser, for redepage v0.1.18
1
+ // Base class for generated parser, for redepage v0.1.19
2
2
 
3
3
  'use strict';
4
4
 
@@ -32,6 +32,7 @@ class BaseParser {
32
32
  dynamic_ = {}; // TODO: extra class
33
33
  prec_ = null;
34
34
  $hasErrors = null;
35
+ leanConditions = {};
35
36
  // trace:
36
37
  trace = [];
37
38
 
@@ -163,6 +164,7 @@ class BaseParser {
163
164
  this._tracePush( [ 'E', true ] );
164
165
  return true;
165
166
  }
167
+ this._tracePush( [ 'E', 0 ] );
166
168
  const match = this._matchesInFollow( type, keyword, 'E' );
167
169
  // If the parser reaches this point with match = null, even the top-level rule
168
170
  // does not have a required token (typically `EOF`) at the end → the parser
@@ -203,8 +205,10 @@ class BaseParser {
203
205
  // cases
204
206
  giR( state, follow ) { // go to state (after trying to test again as identifier)
205
207
  const { keyword } = this.tokens[this.tokenIdx];
206
- if (!keyword || this.keywords[keyword] ||
207
- this._matchesInFollow( 'Id', keyword, 'R' ))
208
+ if (!keyword || this.keywords[keyword])
209
+ return this.g( state, follow );
210
+ this._tracePush( [ 'R', 0 ] );
211
+ if (this._matchesInFollow( 'Id', keyword, 'R' ))
208
212
  return this.g( state, follow );
209
213
  this.nextTokenAsId = true;
210
214
  return false; // do not execute action after it
@@ -256,6 +260,8 @@ class BaseParser {
256
260
 
257
261
  // instead of c() for identifiers, used both with l() and lk()
258
262
  ci( state, ident = 'ident' ) { // consume identifier token
263
+ if (this.tokenIdx === this.fixKeywordTokenIdx)
264
+ return this.e();
259
265
  const la = this.tokens[this.tokenIdx];
260
266
  if (this.keywords[la.keyword])
261
267
  this.reportReservedWord_();
@@ -385,95 +391,104 @@ class BaseParser {
385
391
  return true;
386
392
  }
387
393
 
388
- // predicate used before rule call if with LL(1) conflict, 'Id' in other case
389
- lP( first2 ) { // only start rule if this predicate returns true
394
+ // predicate used before rule call (and called by `ckP` and `gP`) on keyword
395
+ // branch if with weak LL(1) conflict, i.e. there is an 'Id' branch or the
396
+ // default branch has `Id` in its first-set (TODO: or rule end, and `Id` is in
397
+ // follow-union)
398
+ lP( first2 ) {
390
399
  // nothing to check if not a non-reserved keyword:
391
400
  const { keyword: lk1 } = this.tokens[this.tokenIdx];
392
- if (!lk1 || this.keywords[lk1] !== 0)
401
+ if (!lk1 || this.keywords[lk1] !== 0 || this.fixKeywordTokenIdx === this.tokenIdx)
393
402
  return true;
394
403
 
404
+ this._tracePush( [ 'K' ] );
395
405
  const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1];
396
- // Argument first2 is just a performance hint with ckP():
406
+ if (lt2 === 'IllegalToken')
407
+ return true
408
+ // Argument first2 is just a performance hint:
397
409
  if (lk2 && first2?.[0] === 'Id' && !this.keywords[lk2] ||
398
410
  first2?.includes( lk2 || lt2 )) {
399
- this._tracePush( [ 'K', true ] );
411
+ this._traceSubPush( true );
400
412
  return true;
401
413
  }
402
- this._tracePush( [ 'K' ] );
403
414
  // now check it dynamically:
404
- let cmd = this.table[this.s][lk1];
405
- if (cmd[2] !== 1)
406
- throw Error( `Unexpected command '${ cmd?.[0] }' without prediction at state ${ this.s } for ‘${ lk1 }’` );
415
+ if (this._walkPred( this.table[this.s][lk1], lk1, lt2, lk2 ))
416
+ return true;
417
+ this._tracePush( [ 'I' ] );
418
+ const choice = this.table[this.s];
419
+ if (!this._walkPred( choice.Id || choice[''], null, lt2, lk2 ))
420
+ return true;
421
+ this.nextTokenAsId = true;
422
+ return false;
423
+ }
407
424
 
408
- // if not the keyword match, the command is “goto” or “rule call”
409
- const savedState = this.s;
410
- this.s = (cmd[0] === 'ck') ? cmd[1] : this._pred_keyword( cmd[1], lk1 );
425
+ _walkPred( cmd, lk1, lt2, lk2 ) {
426
+ const saved = this._saveForWalk();
427
+ const { length } = this.stack;
428
+ if (typeof cmd[0] !== 'number') // don't skip push to state with rule call
429
+ this.s = cmd[1];
430
+ if (cmd[0] !== (lk1 ? 'ck' : 'ci')) { // make the std case fast
431
+ let match1 = this._pred_next( 'Id', lk1, 'P' ); // TODO: really P for I?
432
+ if (!match1) {
433
+ if (lk1 || match1 === false) // assert for correct code generation
434
+ throw Error( `Cannot match first prediction token in rule at state ${ saved.s }` );
435
+ if (match1 == null) {
436
+ this._traceSubPush( 0 ); // TODO: make _pred_next push this
437
+ match1 = this._matchesInFollow( 'Id', lk1, 'I' );
438
+ }
439
+ else {
440
+ this._traceSubPush( false );
441
+ }
442
+ Object.assign( this, saved );
443
+ this.stack.length = length;
444
+ return !!match1;
445
+ }
446
+ }
411
447
 
448
+ this._traceSubPush( '' ); // between the two tokens
412
449
  ++this.tokenIdx; // for user lookahead fns and conditions
413
- const match = this._pred_next( lt2, lk2, 'K' );
450
+ let match2 = this._pred_next( lt2, lk2, (lk1 ? 'K' : 'I') );
451
+ if (match2 == null) {
452
+ this._traceSubPush( 0 ); // TODO: make _pred_next push this
453
+ match2 = !!this._matchesInFollow( lt2, lk2, (lk1 ? 'K' : 'I') );
454
+ }
455
+ else {
456
+ this._traceSubPush( match2 );
457
+ }
458
+ Object.assign( this, saved );
459
+ this.stack.length = length;
414
460
  --this.tokenIdx;
415
- this.s = savedState;
416
-
417
- const r = match ?? true;
418
- if (match == null)
419
- this._traceSubPush( 0 );
420
- if (lt2 === 'IllegalToken')
421
- return true
422
- // TODO: instead of this IllegalToken test, implement a “confirm unreserved
423
- // keyword as Id” prediction which tests whether the token after the then-Id
424
- // matches.
425
- this._traceSubPush( r );
426
- if (!r)
427
- this.nextTokenAsId = true;
428
- return r;
461
+ return match2;
429
462
  }
430
463
 
431
464
  // Now the helper methods =====================================================
432
465
 
433
466
  // Standard weak-conflict predicate -------------------------------------------
434
467
 
435
- _pred_keyword( state, keyword ) {
436
- // returns state after matching the first token as keyword, for lP()
437
- while (state) {
438
- this._traceSubPush( state );
439
- let cmd = this.table[state];
440
- if (!Array.isArray( cmd ))
441
- cmd = cmd[keyword] || cmd.Id || cmd[''];
442
- switch (cmd[0]) {
443
- case 'ck': case 'mk':
444
- return cmd[1]; // state after token consumption
445
- case 'g': // TODO: another rule call?
446
- break;
447
- default:
448
- if (typeof cmd[0] !== 'number')
449
- throw Error( `Unexpected command ${ cmd[0] } at state ${ this.s }` );
450
- }
451
- state = cmd[1];
452
- }
453
- // reached end of rule without having consumed a token
454
- throw Error( 'Not supported: option for unreserved keywords in follow set' );
455
- }
456
-
457
- _pred_next( type, keyword, mode ) { // mode = K | E | R | M
458
- const useConditions = (mode === 'M'); // TODO: extra method with conditions ?
459
- let hasEnteredRule = false;
468
+ _pred_next( type, keyword, mode ) { // mode = P | K | I | E | R | M
469
+ const properCall = (mode === 'P');
470
+ const lean = (mode !== 'M'); // TODO: extra method with conditions ?
471
+ // TODO: if false, use condition in this.leanConditions
472
+ let hasMatchedToken = null; // undecided yet → calculate on demand
460
473
  while (this.s) {
461
- if (useConditions)
462
- this._tracePush( this.s );
463
- else
474
+ if (lean)
464
475
  this._traceSubPush( this.s );
476
+ else
477
+ this._tracePush( this.s ); // TODO: push new state instead
465
478
  let cmd = this.table[this.s];
466
479
  if (!Array.isArray( cmd )) {
467
480
  const lookahead = cmd[' lookahead'];
468
481
  const c = lookahead // TODO: call with { keyword, type } ?
469
482
  ? cmd[this[lookahead]( mode )]
470
483
  : keyword && cmd[keyword] || cmd[type];
471
- cmd = !(c && useConditions && this._rejectCondition( c, mode )) && c || cmd[''];
484
+ cmd = !(c && this._rejectCondition( c, mode, lean )) && c || cmd[''];
472
485
  }
486
+ const state = this.s;
487
+ this.s = cmd[1];
473
488
  switch (cmd[0]) {
474
489
  case 'c': case 'ck': case 'ckA': // TODO: re-check ckA
475
490
  return true;
476
- case 'ciA':
491
+ case 'ciA': // TODO: fixKeywordTokenIdx ?
477
492
  return mode !== 'R';
478
493
  // in the R prediction for optional `Id<reserved>` at rule end, only
479
494
  // alternative keyword matches are preferred, not identifier matches
@@ -481,7 +496,8 @@ class BaseParser {
481
496
  if (!keyword ||
482
497
  !this.keywords[keyword] && this.fixKeywordTokenIdx !== this.tokenIdx)
483
498
  return mode !== 'R';
484
- cmd = this.table[this.s]['']; // is currently always 'g' or 'e'
499
+ cmd = this.table[state]['']; // is currently always 'g' or 'e'
500
+ this.s = cmd[1];
485
501
  break;
486
502
  case 'm':
487
503
  return type === cmd[2];
@@ -497,17 +513,27 @@ class BaseParser {
497
513
  break;
498
514
  default:
499
515
  if (typeof cmd[0] !== 'number')
500
- throw Error( `Unexpected command ${ cmd[0] } at state ${ this.s }` );
516
+ throw Error( `Unexpected command ${ cmd[0] } at state ${ state }` );
501
517
  // If the parser enters a rule, reaching the rule end (can happen with
502
518
  // option `minTokensMatched`) means "no match".
503
- hasEnteredRule = true;
519
+ hasMatchedToken = false;
504
520
  // If we want to support conditions before matching the first token in a
505
521
  // rule, we would have to handle `this.stack` and `this.dynamically_`.
522
+ if (properCall) {
523
+ // rule_() - TODO: also w/ conditions before matching first token
524
+ this.stack.push( {
525
+ ruleState: cmd[1],
526
+ followState: cmd[0],
527
+ tokenIdx: this.tokenIdx,
528
+ prec: this.prec_,
529
+ } );
530
+ this.dynamic_ = Object.create( this.dynamic_ );
531
+ this.prec_ = null;
532
+ }
506
533
  }
507
534
  // We could optimize with rule call - only 'Id' must be further investigated
508
535
  // TODO: actually also with `g`
509
536
  // in both cases if no condition is evaluated
510
- this.s = cmd[1];
511
537
  // TODO <prepare=…, arg=…> for real trial run also before all returns
512
538
  // if (cmd[5])
513
539
  // this.cmd[5]( cmd[4], mode );
@@ -522,28 +548,31 @@ class BaseParser {
522
548
  // prediction, the tool can normally omit the prediction (and output a
523
549
  // message), no so with `ruleStartingWithUnreserved`. We will rather look
524
550
  // behind the current rule _after_ having decided that the token is to be
525
- // matched as identifier.
526
- return !hasEnteredRule && null; // let caller decide how to interpret this
551
+ // matched as identifier.
552
+ return (hasMatchedToken ?? this.tokenIdx > this.stack.at( -1 ).tokenIdx)
553
+ && null; // let caller decide how to interpret this
527
554
  }
528
555
 
529
- _rejectCondition( cmd, mode ) {
556
+ _rejectCondition( cmd, mode, lean ) {
530
557
  const cond = cmd[3];
531
- if (!cond)
558
+ if (!cond || lean && !this.leanConditions[cond])
532
559
  return false;
533
560
  if (!this.constructor.tracingParser)
534
561
  return !this[cond]( mode, cmd[4] );
535
- // TODO: let this[cond]( true ) return recovery badness in error case
536
- const { traceName } = this[cond];
537
- this._tracePush( [ 'C', traceName?.call( this, cmd[4] ) ?? cond ] );
538
- // calling the condition might have side effects (precendence conditions have)
539
- // call tracing “name” before
562
+ // TODO: let this[cond]( true ) return recovery badness in error case
563
+ if (!lean) {
564
+ const { traceName } = this[cond];
565
+ this._tracePush( [ 'C', traceName?.call( this, cmd[4] ) ?? cond ] );
566
+ // calling the condition might have side effects (precendence conditions have)
567
+ // → call tracing “name” before
568
+ }
540
569
  const fail = !this[cond]( mode, cmd[4] );
541
- this._traceSubPush( !fail );
570
+ this._traceSubPush( lean ? { true: 'C✔', false: 'C✖' }[!fail] : !fail );
542
571
  return fail;
543
572
  }
544
573
 
545
574
  _matchesInFollow( type, keyword, mode ) { // mode = E | R
546
- this._tracePush( [ mode, 0 ] );
575
+ // TODO: now also set stack!
547
576
  const savedState = this.s;
548
577
  // TODO: caching
549
578
  const { dynamic_ } = this;
@@ -564,9 +593,11 @@ class BaseParser {
564
593
  }
565
594
 
566
595
  _confirmExpected( token, saved ) { // mode = M
567
- const [ type, keyword ] = (/^[_a-z]/.test( token )) ? [ 'Id', token ] : [ token ];
596
+ const fix = /^[_a-z]/.test( token );
597
+ const [ type, keyword ] = (fix) ? [ 'Id', token ] : [ token ];
568
598
  Object.assign( this.la(), { type, keyword } );
569
599
  this._cloneFromSaved( saved );
600
+ this.fixKeywordTokenIdx = fix && this.tokenIdx;
570
601
  this.trace = [];
571
602
  let match;
572
603
  while (this.stack.length) {
@@ -639,6 +670,8 @@ class BaseParser {
639
670
  for (const prop in dict) {
640
671
  if (prop && Object.hasOwn( dict, prop ) && prop !== 'Id' &&
641
672
  !Object.hasOwn( expecting, prop ) && prop.charAt(0) !== ' ') {
673
+ // TODO: or call this.translateParserToken_ always?
674
+ // it should then directly set the dictionary -> setTokenInSet_()
642
675
  if (lookahead) { // yes, independently from ckA()
643
676
  for (const p of this.translateParserToken_( prop, lookahead ))
644
677
  expecting[p] = val;
@@ -715,12 +748,14 @@ class BaseParser {
715
748
  delete set[type]; // delete Id if Id token or non-reserved keyword
716
749
 
717
750
  this._trace( 'collect tokens for message,' );
751
+ const { trace } = this;
718
752
  const saved = this._saveForWalk();
719
753
  const expecting = Object.keys( set )
720
754
  .filter( tok => this._confirmExpected( tok, saved ) );
721
755
  token.type = type; // overwritten by _confirmExpected
722
756
  token.keyword = keyword;
723
757
  Object.assign( this, saved );
758
+ this.trace = trace;
724
759
  // TODO: also trace M(…) collection, extra line for each token, with condition
725
760
  return expecting;
726
761
  }
@@ -761,6 +796,7 @@ class BaseParser {
761
796
  // Continue parsing: ignore next predicate (TODO: except some specified ones?)
762
797
  this.conditionTokenIdx = this.tokenIdx;
763
798
  this.conditionStackLength = null;
799
+ this.fixKeywordTokenIdx = -1; // was set when collecting expecting-set
764
800
 
765
801
  // TODO: re-check for rule calls which are at the optional rule end:
766
802
  // x: 'x not'; b: 'b'? x {console.log('x→b')} 'b'?; a: b {console.log('b→a')} 'a'
@@ -887,6 +923,9 @@ class BaseParser {
887
923
 
888
924
  // Predefined conditions with extra option names:
889
925
 
926
+ hide_( mode ) {
927
+ return mode !== 'M';
928
+ }
890
929
  precLeft_( _test, prec ) { // <prec=…>, <…,assoc=left>, <…,prefix=once>
891
930
  const parentPrec = this.stack.at( -1 ).prec;
892
931
  if (parentPrec != null && parentPrec >= prec)