@sap/cds-compiler 4.1.2 → 4.2.4

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 (74) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/cdsc.js +6 -3
  3. package/doc/CHANGELOG_BETA.md +5 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +2 -2
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +24 -24
  8. package/lib/base/message-registry.js +41 -6
  9. package/lib/base/messages.js +7 -0
  10. package/lib/base/model.js +38 -8
  11. package/lib/checks/elements.js +11 -10
  12. package/lib/checks/manyNavigations.js +33 -0
  13. package/lib/checks/onConditions.js +5 -2
  14. package/lib/checks/queryNoDbArtifacts.js +2 -3
  15. package/lib/checks/selectItems.js +4 -55
  16. package/lib/checks/utils.js +3 -2
  17. package/lib/checks/validator.js +3 -1
  18. package/lib/compiler/.eslintrc.json +2 -1
  19. package/lib/compiler/assert-consistency.js +27 -24
  20. package/lib/compiler/base.js +6 -2
  21. package/lib/compiler/builtins.js +34 -34
  22. package/lib/compiler/checks.js +179 -208
  23. package/lib/compiler/classes.js +2 -2
  24. package/lib/compiler/cycle-detector.js +6 -6
  25. package/lib/compiler/define.js +66 -45
  26. package/lib/compiler/extend.js +81 -72
  27. package/lib/compiler/finalize-parse-cdl.js +26 -26
  28. package/lib/compiler/generate.js +61 -45
  29. package/lib/compiler/index.js +47 -49
  30. package/lib/compiler/kick-start.js +8 -7
  31. package/lib/compiler/moduleLayers.js +1 -1
  32. package/lib/compiler/populate.js +42 -35
  33. package/lib/compiler/propagator.js +6 -6
  34. package/lib/compiler/resolve.js +170 -126
  35. package/lib/compiler/shared.js +122 -45
  36. package/lib/compiler/tweak-assocs.js +93 -40
  37. package/lib/compiler/utils.js +15 -12
  38. package/lib/edm/.eslintrc.json +40 -1
  39. package/lib/edm/annotations/genericTranslation.js +721 -707
  40. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  41. package/lib/edm/csn2edm.js +389 -378
  42. package/lib/edm/edm.js +678 -772
  43. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  44. package/lib/edm/edmInboundChecks.js +29 -27
  45. package/lib/edm/edmPreprocessor.js +686 -646
  46. package/lib/edm/edmUtils.js +277 -296
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +1 -1
  49. package/lib/gen/languageParser.js +1253 -1276
  50. package/lib/json/from-csn.js +34 -4
  51. package/lib/json/to-csn.js +4 -4
  52. package/lib/language/language.g4 +2 -5
  53. package/lib/main.d.ts +61 -1
  54. package/lib/model/csnUtils.js +31 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +37 -2
  57. package/lib/modelCompare/utils/filter.js +1 -1
  58. package/lib/optionProcessor.js +15 -3
  59. package/lib/render/toCdl.js +30 -4
  60. package/lib/render/toSql.js +5 -9
  61. package/lib/render/utils/common.js +8 -6
  62. package/lib/transform/db/applyTransformations.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/constraints.js +47 -17
  65. package/lib/transform/db/expansion.js +133 -50
  66. package/lib/transform/db/flattening.js +75 -7
  67. package/lib/transform/forOdata.js +4 -1
  68. package/lib/transform/forRelationalDB.js +80 -62
  69. package/lib/transform/localized.js +91 -54
  70. package/lib/transform/transformUtils.js +9 -10
  71. package/lib/utils/file.js +7 -7
  72. package/lib/utils/moduleResolve.js +210 -121
  73. package/lib/utils/objectUtils.js +1 -1
  74. package/package.json +5 -5
@@ -20,8 +20,8 @@ const {
20
20
  artifactRefLocation,
21
21
  } = require('./utils');
22
22
 
23
- const $inferred = Symbol.for('cds.$inferred');
24
- const $location = Symbol.for('cds.$location');
23
+ const $inferred = Symbol.for( 'cds.$inferred' );
24
+ const $location = Symbol.for( 'cds.$location' );
25
25
 
26
26
  /**
27
27
  * Main export function of this file. Attach "resolve" functions shared for phase
@@ -61,6 +61,7 @@ function fns( model ) {
61
61
  lexical: userBlock,
62
62
  dynamic: modelDefinitions,
63
63
  notFound: undefinedDefinition,
64
+ accept: acceptRealArtifact,
64
65
  },
65
66
  _extensions: {
66
67
  isMainRef: 'all',
@@ -153,9 +154,17 @@ function fns( model ) {
153
154
  notFound: undefinedVariable,
154
155
  param: paramUnsupported,
155
156
  },
157
+ 'limit-rows': {
158
+ lexical: null,
159
+ dollar: true,
160
+ dynamic: () => Object.create( null ),
161
+ notFound: undefinedVariable,
162
+ param: paramSemantics,
163
+ },
164
+ 'limit-offset': 'limit-rows',
156
165
  // general element references -----------------------------------------------
157
166
  where: {
158
- lexical: tableAliasesIfNotExtendAndSelf,
167
+ lexical: tableAliasesAndSelf,
159
168
  dollar: true,
160
169
  dynamic: combinedSourcesOrParentElements,
161
170
  notFound: undefinedSourceElement,
@@ -198,7 +207,8 @@ function fns( model ) {
198
207
  'join-on': {
199
208
  lexical: tableAliasesAndSelf,
200
209
  dollar: true,
201
- dynamic: combinedSourcesOrParentElements, // TODO: source alone...
210
+ dynamic: combinedSourcesOrParentElements,
211
+ rejectRoot: rejectOwnExceptVisibleAliases,
202
212
  notFound: undefinedSourceElement,
203
213
  param: paramSemantics,
204
214
  },
@@ -249,6 +259,7 @@ function fns( model ) {
249
259
  lexical: tableAliasesAndSelf, // TODO: reject own tab aliases
250
260
  dollar: true,
251
261
  dynamic: queryElements,
262
+ rejectRoot: rejectOwnAliasesAndMixins,
252
263
  notFound: undefinedParentElement,
253
264
  check: checkOrderByRef,
254
265
  param: paramSemantics,
@@ -257,6 +268,7 @@ function fns( model ) {
257
268
  lexical: tableAliasesAndSelf, // TODO: reject own tab aliases
258
269
  dollar: true,
259
270
  dynamic: () => Object.create( null ),
271
+ rejectRoot: rejectAllOwn,
260
272
  notFound: undefinedVariable,
261
273
  check: checkRefInQuery,
262
274
  param: paramSemantics,
@@ -290,7 +302,7 @@ function fns( model ) {
290
302
  }
291
303
 
292
304
  if (expr.args) {
293
- const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
305
+ const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
294
306
  // TODO: re-think $expected
295
307
  if (!callback.traverse?.( args, exprCtx, user, callback ))
296
308
  args.forEach( e => traverseExpr( e, exprCtx, user, callback ) );
@@ -326,10 +338,12 @@ function fns( model ) {
326
338
  return `${ art.name.absolute }.${ pathName( ref.path.slice(1) ) }`;
327
339
  }
328
340
 
329
- // Return artifact or element referred by the path in `ref`. The first
330
- // environment we search in is `env`. If no such artifact or element exist,
331
- // complain with message and return `undefined`. Record a dependency from
332
- // `user` to the found artifact if `user` is provided.
341
+ /**
342
+ * Return artifact or element referred by the path in `ref`. The first
343
+ * environment we search in is `env`. If no such artifact or element exist,
344
+ * complain with message and return `undefined`. Record a dependency from
345
+ * `user` to the found artifact if `user` is provided.
346
+ */
333
347
  function resolvePath( ref, expected, user ) {
334
348
  const origUser = user;
335
349
  user = user._user || user;
@@ -348,7 +362,7 @@ function fns( model ) {
348
362
  const semantics = (typeof s === 'string') ? referenceSemantics[s] : s;
349
363
 
350
364
  const r = getPathRoot( ref, semantics, origUser );
351
- const root = r && acceptPathRoot( r, ref, semantics, user );
365
+ const root = r && acceptPathRoot( r, ref, semantics, origUser );
352
366
  if (!root)
353
367
  return setArtifactLink( ref, root );
354
368
 
@@ -373,7 +387,7 @@ function fns( model ) {
373
387
  dependsOn( user._main, target, location, user );
374
388
  }
375
389
  else if (art._main && art.kind !== 'select' || path[0]._navigation?.kind !== '$self') {
376
- // no real dependency to bare $self (or actually: the underlaying query)
390
+ // no real dependency to bare $self (or actually: the underlying query)
377
391
  dependsOn( user, art, location );
378
392
  // Without on-demand resolve, we can simply signal 'undefined "x"'
379
393
  // instead of 'illegal cycle' in the following case:
@@ -415,7 +429,7 @@ function fns( model ) {
415
429
  if (!artifact[par] && i < args.length)
416
430
  artifact[par] = args[i];
417
431
  }
418
- args = args.slice(parameters.length);
432
+ args = args.slice( parameters.length );
419
433
  }
420
434
  else if (args.length > 0 && !typeArtifact?.builtin) {
421
435
  // One or two arguments are interpreted as either length or precision/scale.
@@ -480,7 +494,7 @@ function fns( model ) {
480
494
  : [ '_$next', '$tableAliases' ];
481
495
  // let notApplicable = ...; // for table aliases in JOIN-ON and UNION orderBy
482
496
  for (let env = lexical; env; env = env[nextProp]) {
483
- const dict = env[dictProp] || Object.create(null);
497
+ const dict = env[dictProp] || Object.create( null );
484
498
  const r = dict[head.id];
485
499
  if (acceptLexical( r, path, semantics, user ))
486
500
  return setArtifactLink( head, r );
@@ -542,7 +556,7 @@ function fns( model ) {
542
556
  // TOOD: call envFn with location of last item (for dependency error)
543
557
  const env = envFn( art, path[index - 1].location, user );
544
558
  const found = env && env[item.id]; // not env?.[item.id] ! …we want to keep the 0
545
- // Reject `$self.$_column_1`:
559
+ // Reject `$self.$_column_1`: TODO: necessary to do here again?
546
560
  art = setArtifactLink( item, (found?.name?.$inferred === '$internal') ? undefined : found );
547
561
 
548
562
  if (!art) {
@@ -579,14 +593,13 @@ function fns( model ) {
579
593
  // Non-global lexical are table aliases, mixins and $self, $projection, $parameters,
580
594
  // Do not accept a lonely table alias and `$projection`
581
595
  // TODO: test table alias and mixin named `$projection`
582
- if (path.length === 1 && !user.expand && !user.inline) { // accept lonely…
583
- // allow mixins, and `up_` in anonymous target aspect:
584
- if (art.kind !== '$self' || path[0].id !== '$self')
585
- return art.kind === 'mixin' || art.kind === '$navElement';
586
- return true;
587
- }
588
- // return !art.$internal && art;
589
- return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
596
+ if (path.length !== 1 || user.expand || user.inline)
597
+ return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
598
+
599
+ // allow mixins, $self, and `up_` in anonymous target aspect (is $navElement):
600
+ return art.kind === 'mixin' ||
601
+ art.kind === '$self' && path[0].id === '$self' ||
602
+ art.kind === '$navElement';
590
603
  }
591
604
 
592
605
  function acceptPathRoot( art, ref, semantics, user ) {
@@ -594,6 +607,9 @@ function fns( model ) {
594
607
  const [ head ] = path;
595
608
  if (Array.isArray( art ))
596
609
  return getAmbiguousRefLink( art, head, user );
610
+ if (semantics.rejectRoot?.( art, user, ref, semantics ))
611
+ return null;
612
+
597
613
  switch (art.kind) {
598
614
  case 'using': {
599
615
  const def = model.definitions[art.name.absolute];
@@ -607,8 +623,8 @@ function fns( model ) {
607
623
  return setLink( head, '_navigation', art );
608
624
  }
609
625
  case '$navElement': {
610
- if (head.id === user.$extended)
611
- path.$prefix = user.$extended;
626
+ if (head.id === (user._user || user).$extended)
627
+ path.$prefix = head.id;
612
628
  setLink( head, '_navigation', art );
613
629
  return setArtifactLink( head, art._origin );
614
630
  }
@@ -622,7 +638,7 @@ function fns( model ) {
622
638
  // that the corresponding entity should not be put as $origin into the CSN.
623
639
  // TODO: remove again, should be easy enough in to-csn without.
624
640
  if (path.length === 1 && art.kind === '$tableAlias')
625
- user.$noOrigin = true;
641
+ (user._user || user).$noOrigin = true;
626
642
  return art;
627
643
  }
628
644
  case '$parameters': { // TODO: remove from CC
@@ -644,14 +660,14 @@ function fns( model ) {
644
660
  return false;
645
661
  // only complain about ambiguous source elements if we do not have
646
662
  // duplicate table aliases, only mention non-ambiguous source elems
647
- const uniqueNames = arr.filter( e => !e.$duplicates);
663
+ const uniqueNames = arr.filter( e => !e.$duplicates );
648
664
  if (uniqueNames.length) {
649
665
  const names = uniqueNames.filter( e => e._parent.$inferred !== '$internal' )
650
666
  .map( e => `${ e.name.alias }.${ e.name.element }` );
651
667
  let variant = names.length === uniqueNames.length ? 'std' : 'few';
652
668
  if (names.length === 0)
653
669
  variant = 'none';
654
- error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names });
670
+ error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names } );
655
671
  }
656
672
  return false;
657
673
  }
@@ -693,7 +709,7 @@ function fns( model ) {
693
709
  return user._main || user;
694
710
  // query.$tableAliases contains both aliases and $self/$projection
695
711
  const aliases = query.$tableAliases;
696
- const r = Object.create(null);
712
+ const r = Object.create( null );
697
713
  if (aliases.$self.kind === '$self')
698
714
  r.$self = aliases.$self;
699
715
  // TODO: disallow $projection for ON conditions all together
@@ -773,7 +789,7 @@ function fns( model ) {
773
789
  // default is function `environment`
774
790
 
775
791
  function artifactsEnv( art ) {
776
- return art._subArtifacts || Object.create(null);
792
+ return art._subArtifacts || Object.create( null );
777
793
  }
778
794
 
779
795
  function staticTarget( prev ) {
@@ -787,7 +803,7 @@ function fns( model ) {
787
803
  return target.elements;
788
804
  env = resolvePath( env.targetAspect, 'targetAspect', env );
789
805
  }
790
- return env?.elements || Object.create(null);
806
+ return env?.elements || Object.create( null );
791
807
  }
792
808
 
793
809
  function targetNavigation( art, location, user ) {
@@ -804,14 +820,14 @@ function fns( model ) {
804
820
  // This way (not here though, but later in resolve.js)
805
821
  if (env === 0)
806
822
  return 0;
807
- return env?.elements || Object.create(null);
823
+ return env?.elements || Object.create( null );
808
824
  }
809
825
 
810
826
  function calcElemNavigation( art, location, user ) {
811
827
  const env = navigationEnv( art, location, user, 'calc' );
812
828
  if (env === 0)
813
829
  return 0;
814
- return env?.elements || Object.create(null);
830
+ return env?.elements || Object.create( null );
815
831
  }
816
832
 
817
833
  // Return effective search environment provided by artifact `art`, i.e. the
@@ -825,7 +841,7 @@ function fns( model ) {
825
841
  const env = navigationEnv( art, location, user, 'nav' );
826
842
  if (env === 0)
827
843
  return 0;
828
- return env?.elements || Object.create(null);
844
+ return env?.elements || Object.create( null );
829
845
  }
830
846
 
831
847
  function navigationEnv( art, location, user, assocSpec ) {
@@ -900,7 +916,7 @@ function fns( model ) {
900
916
  const isVar = id.charAt( 0 ) === '$' && id !== '$self';
901
917
  // TODO: for wrong $self, also use ref-undefined-var, but with extra msg id
902
918
  // otherwise, use s/th like ref-unexpected-element
903
- signalNotFound( (isVar ? 'ref-undefined-var' : 'ref-expecting-const'),
919
+ signalNotFound( ( isVar ? 'ref-undefined-var' : 'ref-expecting-const'),
904
920
  [ head.location, user ],
905
921
  valid, { '#': 'std', id } );
906
922
  // TODO: use s/th better than 'ref-expecting-const' !!
@@ -949,7 +965,7 @@ function fns( model ) {
949
965
  function undefinedOrderByElement( user, head, valid, dynamicDict, _art, path ) {
950
966
  const { id } = head;
951
967
  const src = id.charAt( 0 ) !== '$' && user._combined?.[id];
952
- if (src && !Array.isArray(src)) {
968
+ if (src && !Array.isArray( src )) {
953
969
  path.$prefix = src.name.alias; // pushing it to path directly could be problematic
954
970
  // configurable error:
955
971
  signalNotFound( 'ref-deprecated-orderby', [ head.location, user ], valid,
@@ -983,7 +999,7 @@ function fns( model ) {
983
999
  const id = (item === path[path.length - 1])
984
1000
  ? item.id
985
1001
  : pathName( path.slice( path.indexOf( item ) ) );
986
- signalNotFound( (art.$uncheckedElements ? 'ref-unknown-var' : 'ref-undefined-var'),
1002
+ signalNotFound( ( art.$uncheckedElements ? 'ref-unknown-var' : 'ref-undefined-var'),
987
1003
  [ item.location, user ], valid,
988
1004
  { id: `${ art.name.element }.${ id }` } );
989
1005
  }
@@ -1003,6 +1019,56 @@ function fns( model ) {
1003
1019
  // function arguments ( art, user, ref, semantics ),
1004
1020
  // default (for elements only): acceptElemOrVar
1005
1021
 
1022
+ function rejectOwnAliasesAndMixins( art, user, ref, semantics ) { // orderBy-set-ref
1023
+ switch (art.kind) {
1024
+ case '$tableAlias':
1025
+ case 'mixin':
1026
+ if (art._parent !== user)
1027
+ return false;
1028
+ break;
1029
+ case '$self':
1030
+ if (!semantics) // orderBy-set-expr
1031
+ break;
1032
+ // FALLTHROUGH
1033
+ default:
1034
+ return false;
1035
+ }
1036
+ error( 'ref-invalid-element', [ ref.path[0].location, user._user ],
1037
+ { '#': art.kind, id: art.name.id } );
1038
+ return true;
1039
+ }
1040
+
1041
+ function rejectAllOwn( art, user, ref ) { // orderBy-set-expr
1042
+ return rejectOwnAliasesAndMixins( art, user, ref, null );
1043
+ }
1044
+
1045
+ function rejectOwnExceptVisibleAliases( art, user, ref ) { // for join-on
1046
+ switch (art.kind) {
1047
+ case '$navElement':
1048
+ art = art._parent;
1049
+ // FALLTHROUGH
1050
+ case '$tableAlias':
1051
+ case 'mixin':
1052
+ if (art._parent !== user._user || user.$tableAliases[art.name.id])
1053
+ return false;
1054
+ break;
1055
+ case '$self':
1056
+ // in the SQL backend, the $self.elem references are replaced by the
1057
+ // corresponding column expression; this might have references to elements
1058
+ // of invisible table aliases; at least one stakeholder uses this,
1059
+ // so it can't be an error (yet).
1060
+ warning( 'ref-deprecated-self-element', [ ref.path[0].location, user._user ], {},
1061
+ // eslint-disable-next-line max-len
1062
+ 'Referring to the query\'s own elements here might lead to invalid SQL references; use source elements only' );
1063
+ return false;
1064
+ default:
1065
+ return false;
1066
+ }
1067
+ error( 'ref-invalid-element', [ ref.path[0].location, user._user ],
1068
+ { '#': art.kind, id: art.name.id } );
1069
+ return true;
1070
+ }
1071
+
1006
1072
  function acceptElemOrVarOrSelf( art, user, ref ) {
1007
1073
  // TODO: make $self._artifact point to the $self alias, not the entity
1008
1074
  return (!(art._main && art.kind !== 'select') && ref.path[0]._navigation?.kind === '$self')
@@ -1020,7 +1086,7 @@ function fns( model ) {
1020
1086
  }
1021
1087
  else if (art.$requireElementAccess) { // on some CDS variables
1022
1088
  // Path with only one item, but we expect an element, e.g. `$at.from`.
1023
- signalMissingElementAccess(art, [ path[0].location, user ]);
1089
+ signalMissingElementAccess( art, [ path[0].location, user ] );
1024
1090
  return null;
1025
1091
  }
1026
1092
  else if (art.$autoElement) {
@@ -1045,6 +1111,15 @@ function fns( model ) {
1045
1111
  return art;
1046
1112
  }
1047
1113
 
1114
+ function acceptRealArtifact( art, user, ref ) {
1115
+ // For compatibility, we accept `extend Unknown` without elements/actions/includes
1116
+ if (art.kind !== 'namespace' || !(user.elements || user.actions || user.includes))
1117
+ return art;
1118
+ const { location } = ref.path[ref.path.length - 1];
1119
+ signalNotFound( 'ref-undefined-def', [ location, user ], null, { art } );
1120
+ return false;
1121
+ }
1122
+
1048
1123
  function acceptStructOrBare( art, user, ref ) { // for includes[]
1049
1124
  // It had been checked before that `includes` is already forbidden for
1050
1125
  // non-entity/aspect/type/event.
@@ -1069,9 +1144,11 @@ function fns( model ) {
1069
1144
  // Remark: it is not necessary to test for user.elements[$inferred], because
1070
1145
  // the type could only have inferred elements if it has a type expression.
1071
1146
  // Including aspects with elements is forbidden for aspects without the
1072
- // `elements` property (TODO: ensure that, see #11346). Testing for the length of
1073
- // `art.elements` requires that we have applied potential `includes` of
1074
- // `art` before!
1147
+ // `elements` property. Testing for the length of `art.elements` requires
1148
+ // that we have applied potential `includes` of `art` before!
1149
+ // We might allow includes with elements in the future, they'd probably
1150
+ // count as specified elements with lower priority, i.e. annos, types, key
1151
+ // etc on columns beat those inherited from the include.
1075
1152
  if (art.kind === 'aspect' &&
1076
1153
  (!art.elements || base.query && !Object.keys( art.elements ).length))
1077
1154
  return art;
@@ -1482,10 +1559,10 @@ function fns( model ) {
1482
1559
  const err = message( 'ref-expected-element', location,
1483
1560
  { '#': 'magicVar', id: art.name.id } );
1484
1561
  // Mapping for better valid names: from -> $at.from
1485
- const valid = Object.keys(art.elements || {}).reduce((prev, curr) => {
1562
+ const valid = Object.keys( art.elements || {} ).reduce( (prev, curr) => {
1486
1563
  prev[`${ art.name.id }.${ curr }`] = true;
1487
1564
  return prev;
1488
- }, Object.create(null));
1565
+ }, Object.create( null ) );
1489
1566
  attachAndEmitValidNames( err, valid );
1490
1567
  }
1491
1568
 
@@ -1508,14 +1585,14 @@ function fns( model ) {
1508
1585
  // ignore internal types such as cds.Association, ignore names with dot for
1509
1586
  // CDL references to main artifacts:
1510
1587
  if (!art.internal && !art.deprecated && art.name?.$inferred !== '$internal' &&
1511
- (viaCdl ? art._main || !name.includes('.') : art.kind !== 'namespace'))
1588
+ (viaCdl ? art._main || !name.includes( '.' ) : art.kind !== 'namespace'))
1512
1589
  msg.validNames[name] = art;
1513
1590
  }
1514
1591
 
1515
1592
  if (options.testMode && !options.$recompile) {
1516
1593
  // no semantic location => either first of [loc, semantic loc] pair or just location.
1517
1594
  const loc = msg.$location[0] || msg.$location;
1518
- const names = Object.keys(msg.validNames);
1595
+ const names = Object.keys( msg.validNames );
1519
1596
  names.sort();
1520
1597
  if (names.length > 22) {
1521
1598
  names.length = 20;
@@ -1523,7 +1600,7 @@ function fns( model ) {
1523
1600
  }
1524
1601
  info( null, [ loc, null ],
1525
1602
  { '#': !names.length ? 'zero' : 'std' },
1526
- { std: `Valid: ${ names.join(', ') }`, zero: 'No valid names' });
1603
+ { std: `Valid: ${ names.join( ', ' ) }`, zero: 'No valid names' } );
1527
1604
  }
1528
1605
  }
1529
1606
  }
@@ -22,8 +22,8 @@ const {
22
22
  } = require('./utils');
23
23
  const { CsnLocation } = require('./classes');
24
24
 
25
- const $location = Symbol.for('cds.$location');
26
- const $inferred = Symbol.for('cds.$inferred');
25
+ const $location = Symbol.for( 'cds.$location' );
26
+ const $inferred = Symbol.for( 'cds.$inferred' );
27
27
 
28
28
  // Export function of this file.
29
29
  function tweakAssocs( model ) {
@@ -118,7 +118,7 @@ function tweakAssocs( model ) {
118
118
  }
119
119
 
120
120
  function rewriteAssociationCheck( element ) {
121
- const elem = element.items || element; // TODO v2: nested items
121
+ const elem = element.items || element; // TODO v5: nested items
122
122
  if (elem.elements)
123
123
  forEachGeneric( elem, 'elements', rewriteAssociationCheck );
124
124
  if (!elem.target)
@@ -201,7 +201,7 @@ function tweakAssocs( model ) {
201
201
  }
202
202
 
203
203
  function assocWithExplicitSpec( assoc ) {
204
- while (assoc.foreignKeys && inferredForeignKeys( assoc.foreignKeys, 'keys') ||
204
+ while (assoc.foreignKeys && inferredForeignKeys( assoc.foreignKeys, 'keys' ) ||
205
205
  assoc.on && assoc.on.$inferred)
206
206
  assoc = getOrigin( assoc );
207
207
  return assoc;
@@ -264,7 +264,7 @@ function tweakAssocs( model ) {
264
264
  fk.$inferred = 'rewrite'; // Override existing value; TODO: other $inferred value?
265
265
  // TODO: re-check for case that foreign key is managed association
266
266
  if (orig._effectiveType !== undefined)
267
- setLink( fk, '_effectiveType', orig._effectiveType);
267
+ setLink( fk, '_effectiveType', orig._effectiveType );
268
268
  const te = copyExpr( orig.targetElement, elem.location );
269
269
  if (elem._redirected) {
270
270
  const i = te.path[0]; // TODO: or also follow path like for ON?
@@ -273,7 +273,7 @@ function tweakAssocs( model ) {
273
273
  setArtifactLink( te, state );
274
274
  }
275
275
  fk.targetElement = te;
276
- });
276
+ } );
277
277
  if (elem.foreignKeys) // Possibly no fk was set
278
278
  elem.foreignKeys[$inferred] = 'rewrite';
279
279
  }
@@ -288,14 +288,14 @@ function tweakAssocs( model ) {
288
288
  // same (TODO later: set status whether rewrite changes anything),
289
289
  // especially problematic are refs starting with $self:
290
290
  setExpandStatus( elem, 'target' );
291
- if (elem._parent && elem._parent.kind === 'element') {
291
+ if (elem._parent?.kind === 'element') {
292
292
  // managed association as sub element not supported yet
293
293
  error( null, [ elem.location, elem ], {},
294
294
  // eslint-disable-next-line max-len
295
295
  'Rewriting the ON-condition of unmanaged association in sub element is not supported' );
296
296
  return;
297
297
  }
298
- const nav = (elem._main && elem._main.query && elem.value)
298
+ const nav = (elem._main?.query && elem.value)
299
299
  ? pathNavigation( elem.value ) // redirected source elem or mixin
300
300
  : { navigation: assoc }; // redirected user-provided
301
301
  const cond = copyExpr( assoc.on,
@@ -347,15 +347,13 @@ function tweakAssocs( model ) {
347
347
  if (tableAlias) { // from ON cond of element in source ref/d by table alias
348
348
  const source = tableAlias._origin;
349
349
  const root = expr.path[0]._navigation || expr.path[0]._artifact;
350
- // console.log( info(null, [assoc.name.location, assoc],
351
- // { names: expr.path.map(i=>i.id), art: root }, 'TA').toString())
350
+ // console.log( info(null, [ assoc.name.location, assoc ],
351
+ // { names: expr.path.map(i => i.id), art: root }, 'TA').toString());
352
352
  if (!root || root._main !== source)
353
353
  return; // not $self or source element
354
354
  if (expr.scope === 'param' || root.kind === '$parameters')
355
355
  return; // are not allowed anyway - there was an error before
356
- const item = expr.path[root.kind === '$self' ? 1 : 0];
357
- // console.log('YE', assoc.name, item, root.name, expr.path)
358
- const elem = navProjection( item && tableAlias.elements[item.id], assoc );
356
+ const { item, elem } = firstProjectionForPath( expr.path, tableAlias, assoc );
359
357
  rewritePath( expr, item, assoc, elem, assoc.value.location );
360
358
  }
361
359
  else if (assoc._main.query) { // from ON cond of mixin element in query
@@ -386,7 +384,7 @@ function tweakAssocs( model ) {
386
384
  return; // just $self
387
385
  // corresponding elem in including structure
388
386
  const elem = (assoc._main.items || assoc._main).elements[item.id];
389
- if (!(Array.isArray(elem) || // no msg for redefs
387
+ if (!(Array.isArray( elem ) || // no msg for redefs
390
388
  elem === item._artifact || // redirection for explicit def
391
389
  elem._origin === item._artifact)) {
392
390
  const art = assoc._origin;
@@ -399,7 +397,7 @@ function tweakAssocs( model ) {
399
397
  element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)',
400
398
  } );
401
399
  }
402
- rewritePath( expr, item, assoc, (Array.isArray(elem) ? false : elem), null );
400
+ rewritePath( expr, item, assoc, (Array.isArray( elem ) ? false : elem), null );
403
401
  }
404
402
  }
405
403
 
@@ -408,13 +406,12 @@ function tweakAssocs( model ) {
408
406
  let root = path[0];
409
407
  if (!elem) {
410
408
  if (location) {
411
- error( 'rewrite-not-projected', [ location, assoc ],
412
- { name: assoc.name.id, art: item._artifact }, {
413
- // eslint-disable-next-line max-len
414
- std: 'Projected association $(NAME) uses non-projected element $(ART)',
415
- // eslint-disable-next-line max-len
416
- element: 'Projected association $(NAME) uses non-projected element $(MEMBER) of $(ART)',
417
- } );
409
+ error( 'rewrite-not-projected', [ location, assoc ], {
410
+ name: assoc.name.id, art: item._artifact, elemref: { ref: path },
411
+ }, {
412
+ std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)',
413
+ element: 'Projected association $(NAME) uses non-projected element $(MEMBER) of $(ART)',
414
+ } );
418
415
  }
419
416
  delete root._navigation;
420
417
  setArtifactLink( root, elem );
@@ -422,9 +419,15 @@ function tweakAssocs( model ) {
422
419
  return;
423
420
  }
424
421
  if (item !== root) {
422
+ // e.g. mixin ON-condition: Base.foo -> $self.foo or multi-path projection,
423
+ // $projection -> $self
425
424
  root.id = '$self';
426
425
  setLink( root, '_navigation', assoc._parent.$tableAliases.$self );
427
426
  setArtifactLink( root, assoc._parent );
427
+ if (item) {
428
+ const i = path.indexOf(item);
429
+ ref.path = [ root, ...path.slice( i, path.length ) ];
430
+ }
428
431
  }
429
432
  else if (elem.name.id.charAt(0) === '$') {
430
433
  root = { id: '$self', location: item.location };
@@ -470,14 +473,13 @@ function tweakAssocs( model ) {
470
473
  // consider intermediate "preferred" elements - not just `assoc`,
471
474
  // but its origins, too.
472
475
  const proj = navProjection( alias.elements[name], assoc );
473
- name = proj && proj.name && proj.name.id;
476
+ name = proj?.name?.id;
474
477
  if (!name) {
475
478
  if (!forKeys)
476
479
  break;
477
480
  setArtifactLink( item, null );
478
481
  const culprit = elem.target && !elem.target.$inferred && elem.target ||
479
- (elem.value && elem.value.path &&
480
- elem.value.path[elem.value.path.length - 1]) ||
482
+ elem.value?.path?.[elem.value.path.length - 1] ||
481
483
  elem;
482
484
  // TODO: probably better to collect the non-projected foreign keys
483
485
  // and have one message for all
@@ -495,7 +497,7 @@ function tweakAssocs( model ) {
495
497
  env = env.target._artifact?._effectiveType;
496
498
  elem = setArtifactLink( item, env?.elements?.[name] );
497
499
 
498
- if (elem && !Array.isArray(elem))
500
+ if (elem && !Array.isArray( elem ))
499
501
  return elem;
500
502
  // TODO: better (extra message), TODO: do it
501
503
  error( 'query-undefined-element', [ item.location, assoc ],
@@ -516,20 +518,71 @@ function navProjection( navigation, preferred ) {
516
518
  : navigation._projections[0] || null;
517
519
  }
518
520
 
519
- // Return condensed info about reference in select item
520
- // - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
521
- // - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
522
- // - mixinElem -> { navigation: mixinElement, item: path[0] }
523
- // - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
524
- // - $self -> { item: undefined, tableAlias: $self }
525
- // - $parameters.P, :P -> {}
526
- // - $now, current_date -> {}
527
- // - undef, redef -> {}
528
- // With 'navigation': store that navigation._artifact is projected
529
- // With 'navigation': rewrite its ON condition
530
- // With navigation: Do KEY propagation
531
- //
532
- // TODO: re-think this function, copied in populate.js and tweak-assocs.js
521
+
522
+ /**
523
+ * For a path `a.b.c.d`, return a projection for the first path item that is projected.
524
+ * For example, if a query has multiple projections such as `a.b, a, a.b.c`, the
525
+ * _first_ possible projection will be used and the caller can rewrite `a.b.c.d` to `b.c.d`.
526
+ * This avoids that `extend`s affect the ON-condition.
527
+ *
528
+ * The returned object `ret` has `ret.item`, which is the path item that is projected.
529
+ * `ret.elem` is the element projection.
530
+ *
531
+ * @param {any[]} path
532
+ * @param {object} tableAlias
533
+ * @param {object} assoc Preferred association that should be used if projected.
534
+ * @return {{elem: object, item: object}|null}
535
+ */
536
+ function firstProjectionForPath( path, tableAlias, assoc ) {
537
+ const viaSelf = (path[0]._navigation || path[0]._artifact).kind === '$self';
538
+ const root = viaSelf ? 1 : 0;
539
+ if (root >= path.length) // e.g. just `$self` path item
540
+ return { item: undefined, elem: {} };
541
+
542
+ // We want to use the _first_ valid projection that is written by the user (if the preferred
543
+ // `assoc` is not directly projected). To achieve that, look into the table alias' elements.
544
+ const selectedElements = Object.values(tableAlias._parent.elements);
545
+ const proj = [];
546
+ let navItem = tableAlias;
547
+ for (const item of path.slice(root)) {
548
+ navItem = item?.id && navItem.elements?.[item.id];
549
+ if (!navItem) {
550
+ break;
551
+ }
552
+ else if (navItem._projections) {
553
+ const elem = navProjection( navItem, assoc );
554
+ if (elem && elem === assoc) {
555
+ // in case the specified association is found, _always_ use it.
556
+ return { item, elem };
557
+ }
558
+ else if (elem) {
559
+ const index = selectedElements.indexOf(elem);
560
+ proj.push({ item, elem, index });
561
+ }
562
+ }
563
+ }
564
+
565
+ return (proj.length === 0)
566
+ ? { item: path[root], elem: null }
567
+ : proj.reduce( (acc, curr) => (acc.index > curr.index ? curr : acc), proj[0] ); // first
568
+ }
569
+
570
+ /**
571
+ * Return condensed info about reference in select item
572
+ * - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
573
+ * - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
574
+ * - mixinElem -> { navigation: mixinElement, item: path[0] }
575
+ * - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
576
+ * - $self -> { item: undefined, tableAlias: $self }
577
+ * - $parameters.P, :P -> {}
578
+ * - $now, current_date -> {}
579
+ * - undef, redef -> {}
580
+ * With 'navigation': store that navigation._artifact is projected
581
+ * With 'navigation': rewrite its ON condition
582
+ * With navigation: Do KEY propagation
583
+ *
584
+ * TODO: re-think this function, copied in populate.js and tweak-assocs.js
585
+ */
533
586
  function pathNavigation( ref ) {
534
587
  // currently, indirectly projectable elements are not included - we might
535
588
  // keep it this way! If we want them to be included - be aware: cycles