@sap/cds-compiler 4.3.2 → 4.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/lib/api/main.js +14 -24
  3. package/lib/api/options.js +1 -0
  4. package/lib/api/trace.js +38 -0
  5. package/lib/base/location.js +46 -1
  6. package/lib/base/message-registry.js +68 -16
  7. package/lib/base/messages.js +8 -3
  8. package/lib/checks/.eslintrc.json +1 -0
  9. package/lib/checks/actionsFunctions.js +1 -1
  10. package/lib/checks/annotationsOData.js +2 -2
  11. package/lib/checks/selectItems.js +4 -1
  12. package/lib/compiler/assert-consistency.js +3 -2
  13. package/lib/compiler/base.js +1 -1
  14. package/lib/compiler/builtins.js +25 -1
  15. package/lib/compiler/checks.js +6 -5
  16. package/lib/compiler/define.js +12 -10
  17. package/lib/compiler/extend.js +44 -23
  18. package/lib/compiler/finalize-parse-cdl.js +1 -1
  19. package/lib/compiler/generate.js +70 -53
  20. package/lib/compiler/kick-start.js +7 -5
  21. package/lib/compiler/populate.js +31 -22
  22. package/lib/compiler/propagator.js +6 -2
  23. package/lib/compiler/resolve.js +52 -17
  24. package/lib/compiler/shared.js +85 -39
  25. package/lib/compiler/tweak-assocs.js +64 -23
  26. package/lib/compiler/utils.js +40 -23
  27. package/lib/edm/.eslintrc.json +2 -0
  28. package/lib/edm/EdmPrimitiveTypeDefinitions.js +260 -0
  29. package/lib/edm/annotations/edmJson.js +994 -0
  30. package/lib/edm/annotations/genericTranslation.js +82 -423
  31. package/lib/edm/annotations/vocabularyDefinitions.js +160 -0
  32. package/lib/edm/csn2edm.js +12 -5
  33. package/lib/edm/edm.js +14 -73
  34. package/lib/edm/edmPreprocessor.js +6 -0
  35. package/lib/gen/Dictionary.json +187 -16
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +1 -1
  38. package/lib/gen/languageLexer.interp +1 -1
  39. package/lib/gen/languageLexer.js +1129 -671
  40. package/lib/gen/languageParser.js +4285 -4283
  41. package/lib/json/from-csn.js +13 -18
  42. package/lib/json/to-csn.js +11 -6
  43. package/lib/language/antlrParser.js +0 -1
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/errorStrategy.js +95 -30
  46. package/lib/language/genericAntlrParser.js +21 -1
  47. package/lib/main.js +13 -3
  48. package/lib/model/csnRefs.js +42 -8
  49. package/lib/model/csnUtils.js +14 -2
  50. package/lib/model/enrichCsn.js +33 -5
  51. package/lib/model/revealInternalProperties.js +5 -0
  52. package/lib/modelCompare/compare.js +76 -14
  53. package/lib/modelCompare/utils/filter.js +19 -12
  54. package/lib/optionProcessor.js +2 -0
  55. package/lib/render/.eslintrc.json +1 -1
  56. package/lib/render/manageConstraints.js +1 -0
  57. package/lib/render/toHdbcds.js +3 -0
  58. package/lib/render/toRename.js +3 -1
  59. package/lib/render/toSql.js +46 -92
  60. package/lib/render/utils/common.js +76 -0
  61. package/lib/render/utils/delta.js +17 -3
  62. package/lib/sql-identifier.js +1 -1
  63. package/lib/transform/db/.eslintrc.json +1 -0
  64. package/lib/transform/db/applyTransformations.js +30 -4
  65. package/lib/transform/db/associations.js +22 -10
  66. package/lib/transform/db/backlinks.js +6 -2
  67. package/lib/transform/db/expansion.js +2 -2
  68. package/lib/transform/db/transformExists.js +13 -39
  69. package/lib/transform/draft/db.js +14 -3
  70. package/lib/transform/draft/odata.js +5 -18
  71. package/lib/transform/effective/associations.js +46 -15
  72. package/lib/transform/effective/main.js +7 -2
  73. package/lib/transform/effective/misc.js +43 -24
  74. package/lib/transform/effective/queries.js +20 -22
  75. package/lib/transform/effective/types.js +6 -2
  76. package/lib/transform/forOdata.js +10 -3
  77. package/lib/transform/localized.js +1 -1
  78. package/lib/transform/parseExpr.js +73 -21
  79. package/lib/transform/translateAssocsToJoins.js +22 -15
  80. package/lib/utils/term.js +2 -2
  81. package/package.json +2 -1
@@ -214,6 +214,7 @@ function check( model ) {
214
214
 
215
215
  // TODO: This check should be moved to localized.js
216
216
  // "key" keyword at localized element in SELECT list.
217
+ // TODO: not in inferred elements, but also inside aspects
217
218
  if (elem.key?.val && elem._main?.query) {
218
219
  // original element is localized but not key, as that would have
219
220
  // already resulted in a warning by localized.js
@@ -459,7 +460,7 @@ function check( model ) {
459
460
  error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );
460
461
  }
461
462
  }
462
- if (elem.default) {
463
+ if (elem.default?.val !== undefined) {
463
464
  if (elem.targetAspect || elem.on || fkCount !== 1) {
464
465
  const variant = (elem.targetAspect && 'targetAspect') || (elem.on && 'onCond') || 'multi';
465
466
  error( 'type-unexpected-default', [ elem.default.location, elem ], {
@@ -852,7 +853,7 @@ function check( model ) {
852
853
 
853
854
  // Element must exist in annotation
854
855
  if (!elementDecl) {
855
- warning( null, [ anno.location || anno.name.location, art ],
856
+ warning( null, [ anno.location || anno.name.location, art, anno ],
856
857
  { name: anno.name.id, anno: annoDecl.name.id },
857
858
  'Element $(NAME) not found for annotation $(ANNO)' );
858
859
  return;
@@ -866,11 +867,11 @@ function check( model ) {
866
867
  // Must have literal or path unless it is a boolean
867
868
  if (!anno.literal && !anno.path && elementDecl._effectiveType?.category !== 'boolean') {
868
869
  if (elementDecl.type?._artifact) {
869
- warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
870
+ warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ],
870
871
  { '#': 'type', type: elementDecl.type._artifact } );
871
872
  }
872
873
  else {
873
- warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
874
+ warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ],
874
875
  { '#': 'std', anno: anno.name.id } );
875
876
  }
876
877
 
@@ -891,7 +892,7 @@ function check( model ) {
891
892
  return;
892
893
 
893
894
  const anno = annoDef.name.id;
894
- const loc = [ value.location || value.name.location, art ];
895
+ const loc = [ value.location || value.name.location, art, annoDef ];
895
896
 
896
897
  // Array expected?
897
898
  if (elementDecl._effectiveType.items) {
@@ -136,7 +136,7 @@ const {
136
136
  storeExtension,
137
137
  dependsOnSilent,
138
138
  pathName,
139
- isDirectComposition,
139
+ targetCantBeAspect,
140
140
  } = require('./utils');
141
141
  const { compareLayer } = require('./moduleLayers');
142
142
  const { initBuiltins, isInReservedNamespace } = require('./builtins');
@@ -313,7 +313,6 @@ function define( model ) {
313
313
  artifacts[using] = {
314
314
  kind: 'using', // !, not namespace - we do not know artifact yet
315
315
  name: { id: using, location, $inferred: 'as' },
316
- // TODO: use global ref (in general - all uses of splitIntoPath)
317
316
  extern: { location, id: absolute },
318
317
  location,
319
318
  $inferred: 'path-prefix',
@@ -564,7 +563,7 @@ function define( model ) {
564
563
  initMembers( art, art, block );
565
564
  initDollarSelf( art ); // $self
566
565
  if (art.params)
567
- initDollarParameters( art );
566
+ initDollarParameters( art ); // $parameters
568
567
 
569
568
  if (!art.query)
570
569
  return;
@@ -1199,7 +1198,7 @@ function define( model ) {
1199
1198
 
1200
1199
  // To be reworked -------------------------------------------------------------
1201
1200
 
1202
- // TODO: make special for extend/annotate
1201
+ // TODO: is only necessary for extensions - make special for extend/annotate
1203
1202
  function checkDefinitions( construct, parent, prop, dict = construct[prop] ) {
1204
1203
  // TODO: do differently, see also annotateMembers() in resolver
1205
1204
  // To have been checked by parsers:
@@ -1227,7 +1226,7 @@ function define( model ) {
1227
1226
  //
1228
1227
  else if (parent.kind === 'action' || parent.kind === 'function') {
1229
1228
  error( 'ext-unexpected-action', [ construct.location, construct ], { '#': parent.kind }, {
1230
- std: 'Actions and functions can\'t be extended, only annotated',
1229
+ std: 'Actions and functions can\'t be extended, only annotated', // TODO: → ext-unsupported
1231
1230
  action: 'Actions can\'t be extended, only annotated',
1232
1231
  function: 'Functions can\'t be extended, only annotated',
1233
1232
  } );
@@ -1277,17 +1276,20 @@ function define( model ) {
1277
1276
 
1278
1277
  /**
1279
1278
  * Return whether the `target` is actually a `targetAspect`
1280
- * TODO: really do that here and not in kick-start.js?
1281
1279
  */
1282
1280
  function targetIsTargetAspect( elem ) {
1283
1281
  const { target } = elem;
1284
- if (target.elements) {
1285
- // TODO: error if CSN has both target.elements and targetAspect.elements
1286
- // -> delete target
1282
+ if (target.elements) // CSN parser ensures: has no targetAspect then
1287
1283
  return true;
1284
+
1285
+ if (elem.targetAspect) {
1286
+ // Ensure that a compiled CSN is parseable - not inside query, only on element
1287
+ return false;
1288
1288
  }
1289
- if (elem.targetAspect || options.parseCdl || !isDirectComposition( elem ))
1289
+ if (targetCantBeAspect( elem ) || options.parseCdl)
1290
1290
  return false;
1291
+ // Compare this check with check in acceptEntity() called by resolvePath()
1292
+ // Remark: do not check `on` and `foreignKeys` here, we want error for those, not the aspect
1291
1293
  const name = resolveUncheckedPath( target, 'target', elem );
1292
1294
  const aspect = name && model.definitions[name];
1293
1295
  return (aspect?.kind === 'aspect' || aspect?.kind === 'type') && // type is sloppy
@@ -2,7 +2,7 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const { weakLocation } = require('../base/location');
5
+ const { weakRefLocation } = require('../base/location');
6
6
  const { searchName } = require('../base/messages');
7
7
  const {
8
8
  forEachInOrder,
@@ -31,7 +31,22 @@ const $location = Symbol.for( 'cds.$location' );
31
31
  // attach stupid location - TODO: remove in v5
32
32
  const genLocation = new CsnLocation( '' );
33
33
 
34
- // Array.prototype.spread = 42; // prototype-polluted JS classes
34
+ const draftElements = [
35
+ 'IsActiveEntity',
36
+ 'HasActiveEntity',
37
+ 'HasDraftEntity',
38
+ 'DraftAdministrativeData',
39
+ 'SiblingEntity',
40
+ ];
41
+ const draftBoundActions = [
42
+ 'draftPrepare',
43
+ 'draftActivate',
44
+ 'draftEdit',
45
+ ];
46
+
47
+ function canBeDraftMember( name, parent, draftMembers ) {
48
+ return parent?.kind === 'entity' && parent._service && draftMembers.includes( name );
49
+ }
35
50
 
36
51
  function extend( model ) {
37
52
  // Get simplified "resolve" functionality and the message function:
@@ -686,21 +701,15 @@ function extend( model ) {
686
701
  }
687
702
  }
688
703
 
689
- // const unexpected_props = {
690
- // elements: 'anno-unexpected-elements',
691
- // enum: 'anno-unexpected-elements', // TODO
692
- // params: 'anno-unexpected-params',
693
- // actions: 'anno-unexpected-actions',
694
- // };
695
- // const undefined_props = {
696
- // elements: 'anno-undefined-element',
697
- // enum: 'anno-undefined-element', // TODO
698
- // params: 'anno-undefined-param',
699
- // actions: 'anno-undefined-action',
700
- // };
701
-
702
704
  function checkRemainingMemberExtensions( parent, ext, prop, name ) {
703
705
  // console.log('CRME:',prop,name,parent,ext)
706
+
707
+ // TODO: just use `ext-undefined-element` etc also when no elements are there
708
+ // at all (but use an extra text variant and the `{…}` location). Reason: we
709
+ // might allow to add new actions, and an `annotate` on an undefined action
710
+ // should not lead to another message id. We would use and extra message id
711
+ // if we consider this an error or such sub annotates are then ignored
712
+ // (i.e. not put into the "super annotate").
704
713
  const dict = parent[prop];
705
714
  if (!dict) {
706
715
  // TODO: check - for each name? - better locations
@@ -714,6 +723,9 @@ function extend( model ) {
714
723
  { '#': (parent._effectiveType?.kind === 'entity') ? 'entity' : 'std' }, {
715
724
  std: 'Elements only exist in entities, types or typed constructs',
716
725
  entity: 'Elements of entity types can\'t be annotated',
726
+ // TODO: extra msg for 'entity'? → this is some other
727
+ // situation, somehow similar when trying to annotate elements
728
+ // of target entity
717
729
  } );
718
730
  break;
719
731
  case 'params':
@@ -721,8 +733,10 @@ function extend( model ) {
721
733
  'Parameters only exist for actions or functions' );
722
734
  break;
723
735
  case 'actions':
724
- // TODO: check if artifact can have actions, similar to `anno-unexpected-actions`
725
- notFound( 'anno-undefined-action', ext.name.location, ext,
736
+ if (canBeDraftMember( name, parent, draftBoundActions ))
737
+ return true;
738
+ // TODO: use extra text variant and location of dictionary - no
739
+ notFound( 'ext-undefined-action', ext.name.location, ext,
726
740
  { '#': 'action', art: parent, name } );
727
741
  break;
728
742
  default:
@@ -737,22 +751,26 @@ function extend( model ) {
737
751
  const art = inReturns || parent;
738
752
  switch (prop) {
739
753
  case 'elements':
740
- notFound( 'anno-undefined-element', ext.name.location, ext,
754
+ if (canBeDraftMember( name, parent, draftElements ))
755
+ break;
756
+ notFound( 'ext-undefined-element', ext.name.location, ext,
741
757
  { '#': (inReturns ? 'returns' : 'element'), art, name },
742
758
  parent.elements );
743
759
  break;
744
760
  case 'enum': // TODO: extra msg id?
745
- notFound( 'anno-undefined-element', ext.name.location, ext,
761
+ notFound( 'ext-undefined-element', ext.name.location, ext,
746
762
  { '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
747
763
  parent.enum );
748
764
  break;
749
765
  case 'params':
750
- notFound( 'anno-undefined-param', ext.name.location, ext,
766
+ notFound( 'ext-undefined-param', ext.name.location, ext,
751
767
  { '#': 'param', art: parent, name },
752
768
  parent.params );
753
769
  break;
754
770
  case 'actions':
755
- notFound( 'anno-undefined-action', ext.name.location, ext,
771
+ if (canBeDraftMember( name, parent, draftBoundActions ))
772
+ break;
773
+ notFound( 'ext-undefined-action', ext.name.location, ext,
756
774
  { '#': 'action', art: parent, name },
757
775
  parent.actions );
758
776
  break;
@@ -1153,8 +1171,10 @@ function extend( model ) {
1153
1171
  // Warning 'Overwrites definition from include "I" (at elem def)
1154
1172
  const parent = ext === art && art;
1155
1173
  const members = ext[prop];
1156
- if (members)
1174
+ if (members) {
1157
1175
  ext[prop] = Object.create( null );
1176
+ ext[prop][$location] = members[$location];
1177
+ }
1158
1178
  let hasNewElement = false;
1159
1179
 
1160
1180
  for (const ref of ext.includes) {
@@ -1162,6 +1182,7 @@ function extend( model ) {
1162
1182
  if (template) { // be robust
1163
1183
  if (template[prop] && !ext[prop])
1164
1184
  ext[prop] = Object.create( null );
1185
+ const location = weakRefLocation( ref );
1165
1186
  // eslint-disable-next-line no-loop-func
1166
1187
  forEachInOrder( template, prop, ( origin, name ) => {
1167
1188
  if (members && members[name]) {
@@ -1170,7 +1191,7 @@ function extend( model ) {
1170
1191
  return;
1171
1192
  }
1172
1193
  hasNewElement = true;
1173
- const elem = linkToOrigin( origin, name, parent, prop, weakLocation( ref.location ) );
1194
+ const elem = linkToOrigin( origin, name, parent, prop, location );
1174
1195
  setLink( elem, '_block', origin._block );
1175
1196
  if (!parent) // not yet set for EXTEND foo WITH bar => linkToOrigin() did not add it
1176
1197
  dictAdd( ext[prop], name, elem );
@@ -86,7 +86,7 @@ function finalizeParseCdl( model ) {
86
86
  resolveUncheckedPath( include, 'include', main );
87
87
 
88
88
  // define.js takes care that `target` is a ref and
89
- // `targetAspect` is a structure.
89
+ // `targetAspect` is a structure (anonymous aspect) or ref to aspect.
90
90
  if (artifact.target)
91
91
  resolveUncheckedPath( artifact.target, 'target', main );
92
92
  if (artifact.targetAspect)
@@ -17,7 +17,9 @@ const {
17
17
  isDirectComposition,
18
18
  copyExpr,
19
19
  } = require('./utils');
20
- const { weakLocation } = require('../base/location');
20
+ const { weakLocation, weakRefLocation, weakEndLocation } = require('../base/location');
21
+
22
+ const $location = Symbol.for( 'cds.$location' );
21
23
 
22
24
  function generate( model ) {
23
25
  const { options } = model;
@@ -220,6 +222,7 @@ function generate( model ) {
220
222
  warning( 'def-ignoring-localized', [ errpos.location, elem ],
221
223
  { keyword: 'localized' },
222
224
  'Keyword $(KEYWORD) is ignored for primary keys' );
225
+ // continuation semantics as stated: counts as key field in texts entity
223
226
  }
224
227
  }
225
228
  if (textElems.length <= keys)
@@ -277,11 +280,11 @@ function generate( model ) {
277
280
  * @param {boolean} fioriEnabled
278
281
  */
279
282
  function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
280
- const { location } = base.name;
283
+ const location = weakLocation( base.elements[$location] || base.location );
281
284
  const art = {
282
285
  kind: 'entity',
283
286
  name: { id: absolute, location },
284
- location: base.location,
287
+ location,
285
288
  elements: Object.create( null ),
286
289
  $inferred: 'localized-entity',
287
290
  };
@@ -332,30 +335,8 @@ function generate( model ) {
332
335
  // assertUnique array value, first entry is 'locale'
333
336
  const assertUniqueValue = [];
334
337
 
335
- for (const orig of textElems) {
336
- const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
337
- if (orig.key && orig.key.val) {
338
- // elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
339
- // TODO: the previous would be better, but currently not supported in toCDL
340
- if (!fioriEnabled) {
341
- elem.key = { val: true, $inferred: 'localized', location };
342
- // If the propagated elements remain key (that is not fiori.draft.enabled)
343
- // they should be omitted from OData containment EDM
344
- setAnnotation( elem, '@odata.containment.ignore', location );
345
- }
346
- else {
347
- // add the former key paths to the unique constraint
348
- assertUniqueValue.push({
349
- path: [ { id: orig.name.id, location: orig.location } ],
350
- location: orig.location,
351
- });
352
- }
353
- }
354
- if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
355
- const localized = orig.localized || orig.type || orig.name;
356
- elem.localized = { val: null, $inferred: 'localized', location: localized.location };
357
- }
358
- }
338
+ for (const orig of textElems)
339
+ addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue );
359
340
 
360
341
  initArtifact( art );
361
342
  if (art.includes) {
@@ -366,7 +347,7 @@ function generate( model ) {
366
347
  if (fioriEnabled) {
367
348
  // The includes mechanism puts TextsAspect's elements before .texts' elements.
368
349
  // Because ID_texts is not copied from TextsAspect, the order is messed
369
- // up. Fix it.
350
+ // up. Fix it. TODO: introduce $includeAfter from Extensions.md
370
351
  const { elements } = art;
371
352
  art.elements = Object.create( null );
372
353
  const names = [ 'ID_texts', 'locale', ...Object.keys( elements ) ];
@@ -385,6 +366,31 @@ function generate( model ) {
385
366
  return art;
386
367
  }
387
368
 
369
+ function addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue ) {
370
+ const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
371
+ // To keep the locations of non-inferred original elements, do not set $inferred:
372
+ if (orig.$inferred)
373
+ elem.$inferred = 'localized-origin';
374
+ const { location } = elem;
375
+ if (orig.key && orig.key.val) {
376
+ // elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
377
+ // TODO: the previous would be better, but currently not supported in toCDL
378
+ if (!fioriEnabled) {
379
+ elem.key = { val: true, $inferred: 'localized', location };
380
+ // If the propagated elements remain key (that is not fiori.draft.enabled)
381
+ // they should be omitted from OData containment EDM
382
+ setAnnotation( elem, '@odata.containment.ignore', location );
383
+ }
384
+ else {
385
+ // add the former key paths to the unique constraint
386
+ assertUniqueValue.push( { path: [ { id: orig.name.id, location } ], location } );
387
+ }
388
+ }
389
+ if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
390
+ elem.localized = { val: null, $inferred: 'localized', location };
391
+ }
392
+ }
393
+
388
394
  /**
389
395
  * Enrich the `.texts` entity for the given base artifact.
390
396
  * In contrast to createTextsEntityWithDefaultElements(), this one creates
@@ -400,14 +406,15 @@ function generate( model ) {
400
406
  function enrichTextsEntityWithInclude( art, base, absolute, fioriEnabled ) {
401
407
  const textsAspectName = 'sap.common.TextsAspect';
402
408
  const textsAspect = model.definitions['sap.common.TextsAspect'];
403
- const { location } = art.name;
409
+ const { location } = art;
404
410
 
405
- art.includes = [ createInclude( textsAspectName, base.location ) ];
411
+ art.includes = [ createInclude( textsAspectName, location ) ];
406
412
 
407
413
  if (fioriEnabled) {
408
414
  // "Early" include; only for element `locale`, which has its `key` property
409
415
  // removed (or rather: it is not copied).
410
416
  linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location );
417
+ art.elements.locale.$inferred = 'localized';
411
418
  }
412
419
 
413
420
  if (addTextsLanguageAssoc && art.elements.language)
@@ -425,12 +432,13 @@ function generate( model ) {
425
432
  // If there is a type `sap.common.Locale`, then use it as the type for the element `locale`.
426
433
  // If not, use the default `cds.String` with a length of 14.
427
434
  const hasLocaleType = model.definitions['sap.common.Locale']?.kind === 'type';
428
- const { location } = art.name;
435
+ const { location } = art; // is already a weak location
429
436
  const locale = {
430
437
  name: { location, id: 'locale' },
431
438
  kind: 'element',
432
439
  type: linkMainArtifact( location, hasLocaleType ? 'sap.common.Locale' : 'cds.String' ),
433
440
  location,
441
+ $inferred: 'localized', // $generated in Universal CSN, no $location
434
442
  };
435
443
  if (!hasLocaleType)
436
444
  locale.length = { literal: 'number', val: 14, location };
@@ -449,7 +457,7 @@ function generate( model ) {
449
457
  // texts : Composition of many Books.texts on texts.ID=ID;
450
458
  /** @type {array} */
451
459
  const keys = textElems.filter( e => e.key && e.key.val );
452
- const { location } = art.name;
460
+ const location = weakEndLocation( art.elements[$location] ) || weakLocation( art.location );
453
461
  const texts = {
454
462
  name: { location, id: 'texts' },
455
463
  kind: 'element',
@@ -568,7 +576,7 @@ function generate( model ) {
568
576
  while (origin && !origin.targetAspect && origin._origin)
569
577
  origin = origin._origin;
570
578
  let target = origin.targetAspect;
571
- if (target && target.path)
579
+ if (target?.path)
572
580
  target = resolvePath( origin.targetAspect, 'targetAspect', origin );
573
581
  if (!target || !target.elements)
574
582
  return;
@@ -601,7 +609,7 @@ function generate( model ) {
601
609
  function allowAspectComposition( target, elem, keys, entityName ) {
602
610
  if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates ))
603
611
  return false; // no elements or with redefinitions
604
- const location = elem.target && elem.target.location || elem.location;
612
+ const location = elem.targetAspect?.location || elem.location;
605
613
  if ((elem._main._upperAspects || []).includes( target ))
606
614
  return 0; // circular containment of the same aspect
607
615
 
@@ -648,16 +656,22 @@ function generate( model ) {
648
656
  if (elem.type && !isDirectComposition( elem )) {
649
657
  // Only issue warning for direct usages, not for projections, includes, etc.
650
658
  // TODO: Make it configurable error; v5: error
651
- warning( 'def-expected-comp-aspect', [ elem.type.location, elem ],
652
- { prop: 'Composition of', otherprop: 'Association to' },
653
- 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect' );
659
+ // TODO: move to resolve.js where we test the targetAspect,
660
+ warning( 'type-expecting-composition', [ elem.type.location, elem ],
661
+ { newcode: 'Composition of', code: 'Association to' },
662
+ 'Expecting $(NEWCODE), not $(CODE) for the anonymous target aspect' );
663
+ // auto-correct to avoid additional error 'type-unexpected-target-aspect' if
664
+ // cds.Association:
665
+ const { path, $inferred } = elem.type;
666
+ if (!$inferred && path?.length === 1 && path[0].id === 'cds.Association')
667
+ path[0].id = 'cds.Composition';
654
668
  }
655
669
 
656
670
  return true;
657
671
  }
658
672
 
659
673
  function createTargetEntity( target, elem, keys, entityName, base ) {
660
- const { location } = elem.targetAspect || elem.target || elem;
674
+ const location = weakRefLocation( elem.targetAspect || elem.target || elem );
661
675
  elem.on = {
662
676
  location,
663
677
  op: { val: '=', location },
@@ -673,7 +687,7 @@ function generate( model ) {
673
687
  name: {
674
688
  id: entityName,
675
689
  // for code navigation (e.g. via `extend`s): point to the element's name
676
- location: elem.name.location,
690
+ location: weakLocation( elem.name.location ),
677
691
  },
678
692
  location,
679
693
  elements: Object.create( null ),
@@ -690,18 +704,17 @@ function generate( model ) {
690
704
  }
691
705
 
692
706
  // Since there is no user-written up_ element, use a weak location to the beginning of {…}.
693
- const upLocation = weakLocation( location );
694
707
  const up = { // elements.up_ = ...
695
- name: { location: upLocation, id: 'up_' },
708
+ name: { location, id: 'up_' },
696
709
  kind: 'element',
697
- location: upLocation,
710
+ location,
698
711
  $inferred: 'aspect-composition',
699
- type: linkMainArtifact( upLocation, 'cds.Association' ),
700
- target: linkMainArtifact( upLocation, base.name.id ),
712
+ type: linkMainArtifact( location, 'cds.Association' ),
713
+ target: linkMainArtifact( location, base.name.id ),
701
714
  cardinality: {
702
- targetMin: { val: 1, literal: 'number', location: upLocation },
703
- targetMax: { val: 1, literal: 'number', location: upLocation },
704
- location: upLocation,
715
+ targetMin: { val: 1, literal: 'number', location },
716
+ targetMax: { val: 1, literal: 'number', location },
717
+ location,
705
718
  },
706
719
  };
707
720
  // By default, 'up_' is a managed primary key association.
@@ -710,18 +723,21 @@ function generate( model ) {
710
723
  if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) {
711
724
  addProxyElements( art, keys, 'aspect-composition', target.name && location,
712
725
  'up__', '@odata.containment.ignore' );
713
- up.on = augmentEqual( upLocation, 'up_', Object.values( keys ), 'up__' );
726
+ up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
714
727
  }
715
728
  else {
716
- up.key = { location: upLocation, val: true };
729
+ up.key = { location, val: true };
717
730
  // managed associations must be explicitly set to not null
718
731
  // even if target cardinality is 1..1
719
- up.notNull = { location: upLocation, val: true };
732
+ up.notNull = { location, val: true };
720
733
  }
721
734
 
722
735
  dictAdd( art.elements, 'up_', up );
723
736
  // Only for named aspects, use a new location; otherwise use the origin's one.
724
- addProxyElements( art, target.elements, 'aspect-composition', target.name && location );
737
+
738
+ // To keep the locations of non-inferred original elements, do not set $inferred:
739
+ const enforceLocation = target.name || elem.$inferred;
740
+ addProxyElements( art, target.elements, 'aspect-composition', enforceLocation && location );
725
741
 
726
742
  setLink( art, '_block', model.$internal );
727
743
  model.definitions[entityName] = art;
@@ -742,7 +758,8 @@ function generate( model ) {
742
758
  const origin = elements[name];
743
759
  const proxy = linkToOrigin( origin, pname, null, null, location, true );
744
760
  setLink( proxy, '_block', origin._block );
745
- proxy.$inferred = inferred;
761
+ if (location)
762
+ proxy.$inferred = inferred;
746
763
  if (origin.masked)
747
764
  proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked );
748
765
  if (origin.key)
@@ -784,7 +801,7 @@ function generate( model ) {
784
801
  }
785
802
 
786
803
  function linkMainArtifact( location, absolute ) {
787
- const r = { location };
804
+ const r = { location, path: [ { id: absolute, location } ] };
788
805
  setArtifactLink( r, model.definitions[absolute] );
789
806
  return r;
790
807
  }
@@ -3,7 +3,12 @@
3
3
  'use strict';
4
4
 
5
5
  const { isBetaEnabled, forEachGeneric } = require('../base/model');
6
- const { setLink, annotationVal, annotationIsFalse } = require('./utils');
6
+ const {
7
+ setLink,
8
+ annotationVal,
9
+ annotationIsFalse,
10
+ isDirectComposition,
11
+ } = require('./utils');
7
12
 
8
13
  function kickStart( model ) {
9
14
  const { options } = model;
@@ -122,10 +127,7 @@ function kickStart( model ) {
122
127
  }
123
128
 
124
129
  function tagCompositionTargets( elem ) {
125
- const { type } = elem;
126
- if (elem.target && type &&
127
- (type._artifact === model.definitions['cds.Composition'] ||
128
- type.path?.[0].id === 'cds.Composition')) {
130
+ if (elem.target && isDirectComposition( elem )) {
129
131
  // A target aspect would have already moved to property `targetAspect` in
130
132
  // define.js (hm... more something for kick-start.js...)
131
133
  // TODO: for safety, just use resolveUncheckedPath()