@sap/cds-compiler 6.5.0 → 6.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bin/cdsc.js +1 -1
  3. package/lib/api/main.js +0 -3
  4. package/lib/base/builtins.js +2 -1
  5. package/lib/base/message-registry.js +13 -14
  6. package/lib/base/model.js +0 -1
  7. package/lib/checks/sql-snippets.js +8 -0
  8. package/lib/checks/validator.js +4 -2
  9. package/lib/compiler/define.js +102 -115
  10. package/lib/compiler/extend.js +67 -37
  11. package/lib/compiler/finalize-parse-cdl.js +1 -1
  12. package/lib/compiler/generate.js +34 -11
  13. package/lib/compiler/index.js +2 -2
  14. package/lib/compiler/kick-start.js +26 -35
  15. package/lib/compiler/populate.js +4 -7
  16. package/lib/compiler/propagator.js +20 -1
  17. package/lib/compiler/resolve.js +26 -1
  18. package/lib/compiler/shared.js +49 -9
  19. package/lib/compiler/utils.js +2 -2
  20. package/lib/edm/annotations/edmJson.js +111 -37
  21. package/lib/edm/annotations/genericTranslation.js +3 -1
  22. package/lib/main.d.ts +0 -3
  23. package/lib/main.js +2 -0
  24. package/lib/model/csnRefs.js +6 -1
  25. package/lib/render/toSql.js +0 -4
  26. package/lib/transform/db/applyTransformations.js +1 -1
  27. package/lib/transform/db/associations.js +24 -15
  28. package/lib/transform/db/flattening.js +1 -1
  29. package/lib/transform/db/views.js +0 -39
  30. package/lib/transform/effective/associations.js +5 -55
  31. package/lib/transform/effective/main.js +4 -2
  32. package/lib/transform/effective/misc.js +1 -1
  33. package/lib/transform/effective/types.js +36 -12
  34. package/lib/transform/forOdata.js +126 -3
  35. package/lib/transform/forRelationalDB.js +13 -4
  36. package/lib/transform/transformUtils.js +51 -1
  37. package/lib/transform/translateAssocsToJoins.js +43 -19
  38. package/package.json +1 -1
@@ -78,6 +78,7 @@ function extend( model ) {
78
78
  extendArtifactBefore,
79
79
  extendArtifactAfter,
80
80
  extendForeignKeys,
81
+ withLocalizedData,
81
82
  applyIncludes, // TODO: re-check
82
83
  } );
83
84
 
@@ -864,19 +865,18 @@ function extend( model ) {
864
865
  }
865
866
  else if (!dict?.[name]) {
866
867
  // TODO: make variant `returns` an auto-variant for ($ART) ?
867
- const inReturns = parent._parent?.returns && parent._parent;
868
- const art = inReturns || parent;
868
+ const inReturns = parent._parent?.returns;
869
869
  switch (prop) {
870
870
  case 'elements':
871
871
  if (canBeDraftMember( name, parent, draftElements ))
872
872
  break;
873
873
  notFound( `ext-undefined-element${ securityRelevant }`, ext.name.location, ext,
874
- { '#': (inReturns ? 'returns' : 'element'), art, name },
874
+ { '#': (inReturns ? 'returns' : 'element'), name },
875
875
  parent.elements );
876
876
  break;
877
877
  case 'enum': // TODO: extra msg id?
878
878
  notFound( `ext-undefined-element${ securityRelevant }`, ext.name.location, ext,
879
- { '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
879
+ { '#': (inReturns ? 'enum-returns' : 'enum'), name },
880
880
  parent.enum );
881
881
  break;
882
882
  case 'foreignKeys':
@@ -885,14 +885,14 @@ function extend( model ) {
885
885
  break;
886
886
  case 'params':
887
887
  notFound( `ext-undefined-param${ securityRelevant }`, ext.name.location, ext,
888
- { '#': 'param', art: parent, name },
888
+ { '#': 'param', name },
889
889
  parent.params );
890
890
  break;
891
891
  case 'actions':
892
892
  if (canBeDraftMember( name, parent, draftBoundActions ))
893
893
  break;
894
894
  notFound( `ext-undefined-action${ securityRelevant }`, ext.name.location, ext,
895
- { '#': 'action', art: parent, name },
895
+ { '#': 'action', name },
896
896
  parent.actions );
897
897
  break;
898
898
  default:
@@ -941,32 +941,28 @@ function extend( model ) {
941
941
  }
942
942
 
943
943
  function checkRemainingMainExtensions( art, ext ) {
944
- const refCtx = ext.kind === 'annotate' && hasSecurityAnno( ext )
945
- ? 'annotate-sec'
946
- : ext.kind;
944
+ const refCtx = extensionRefContext( ext );
947
945
  if (!resolvePath( ext.name, refCtx, ext )) // error for extend, info for annotate
948
946
  return;
949
947
 
950
- if (art?.builtin) {
948
+ if (art?.builtin) { // TODO: do via accept
951
949
  info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location?
952
950
  }
953
- else if (ext.kind === 'extend' && art?.kind === 'namespace') {
954
- // `annotate` on namespaces already handled before
955
- const hasAnnotations = Object.keys(ext).find(a => a.charAt(0) === '@');
956
- const firstAnno = ext[hasAnnotations];
957
- // In v5, extending namespaces is only allowed for `extend with definitions`.
958
- // Neither annotations nor other extensions are allowed.
959
- // Non-artifact extensions are reported in resolvePath() already (for v5).
960
- // Because "namespaces" are the same as "unknown" artifacts in CSN, we don't report
961
- // an error for `annotate`s.
962
- // FIXME: The compiler generates empty `annotate` statements for
963
- // `extend ns with definitions {…}`. That's why we check the frontend.
964
- if (hasAnnotations || (!ext.artifacts && ext._block.$frontend !== 'json')) {
965
- error( 'ref-undefined-art', [ (firstAnno?.name || ext.name).location, ext ], {
966
- '#': 'namespace', art: ext,
967
- } );
968
- }
951
+ }
952
+
953
+ function extensionRefContext( ext ) {
954
+ if (ext.kind === 'annotate') {
955
+ if (hasSecurityAnno( ext ))
956
+ return 'annotate-sec';
957
+ }
958
+ else if (ext.artifacts || // extend with definitions
959
+ ext._block.$frontend === 'json' && !ext.elements && !ext.actions) {
960
+ // TODO: not for `extend context` and `extend service` !ext.expectedKind
961
+ // TODO v7: also fully with CSN input
962
+ if (!ext.doc && !Object.keys( ext ).some( a => a.charAt(0) === '@') )
963
+ return 'annotate'; // TODO: or an extra refCtx ?
969
964
  }
965
+ return ext.kind;
970
966
  }
971
967
 
972
968
  // Issue messages for annotations on namespaces and builtins
@@ -1213,11 +1209,9 @@ function extend( model ) {
1213
1209
  *
1214
1210
  * If not for extensions: construct === parent
1215
1211
  *
1216
- * Param `initExtensions` is for parse.cdl - TODO delete
1217
- *
1218
1212
  * TODO: separate extension!
1219
1213
  */
1220
- function initMembers( construct, parent, block, initExtensions = false ) {
1214
+ function initMembers( construct, parent, block ) {
1221
1215
  // TODO: split extend from init
1222
1216
  const main = parent._main || parent;
1223
1217
  const isQueryExtension = construct.kind === 'extend' && main.query;
@@ -1347,9 +1341,12 @@ function extend( model ) {
1347
1341
  elem.name = { id: name, location: elem.location };
1348
1342
  }
1349
1343
  }
1344
+
1345
+ if (!elem._block)
1346
+ setLink( elem, '_block', block );
1350
1347
  // if (!kindProperties[ elem.kind ]) console.log(elem.kind,elem.name)
1351
- if ((elem.kind === 'extend' || elem.kind === 'annotate') && !initExtensions) {
1352
- storeExtension( elem, name, prop, parent, block );
1348
+ if ((elem.kind === 'extend' || elem.kind === 'annotate')) {
1349
+ storeExtension( elem, name, prop, parent );
1353
1350
  return;
1354
1351
  }
1355
1352
  if (isQueryExtension && elem.kind === 'element') {
@@ -1359,8 +1356,6 @@ function extend( model ) {
1359
1356
  return;
1360
1357
  }
1361
1358
 
1362
- const bl = elem._block || block;
1363
- setLink( elem, '_block', bl );
1364
1359
  const existing = parent[prop]?.[name];
1365
1360
  const add = construct !== parent && (!existing || elem.$inferred !== 'include');
1366
1361
  // don't dump with `entity T {}; extend T with { extend e {}; e {}; e {} };`:
@@ -1368,7 +1363,7 @@ function extend( model ) {
1368
1363
  elem.$duplicates = null;
1369
1364
  setMemberParent( elem, name, parent, add && prop );
1370
1365
  checkRedefinition( elem );
1371
- initMembers( elem, elem, bl, initExtensions );
1366
+ initMembers( elem, elem, elem._block );
1372
1367
  if (elem.kind === 'action' || elem.kind === 'function')
1373
1368
  initBoundSelfParam( elem.params, elem._main );
1374
1369
 
@@ -1514,7 +1509,7 @@ function extend( model ) {
1514
1509
  * Sets `_ancestors` links on `art`.
1515
1510
  *
1516
1511
  * TODO: try to set `_ancestors` only to entities (but beware “intermediate”
1517
- * non-entities).
1512
+ * non-entities - TODO: make intermediate non-entities stop chain).
1518
1513
  *
1519
1514
  * Examples:
1520
1515
  * ext === art: `entity E : F {}` => add elements of F to E
@@ -1532,10 +1527,11 @@ function extend( model ) {
1532
1527
 
1533
1528
  if (!art._ancestors && !art.query)
1534
1529
  setLink( art, '_ancestors', [] ); // recursive array of includes
1530
+ const shouldSetAncestors = art.kind === 'entity' && !art.query;
1535
1531
  for (const ref of ext.includes) {
1536
1532
  const template = resolvePath( ref, 'include', art );
1537
1533
  // !template -> non-includable, e.g. scalar type, or cyclic
1538
- if (template && !art.query) {
1534
+ if (template?.kind === 'entity' && shouldSetAncestors) {
1539
1535
  if (template._ancestors)
1540
1536
  art._ancestors.push( ...template._ancestors );
1541
1537
  art._ancestors.push( template );
@@ -1583,8 +1579,17 @@ function extend( model ) {
1583
1579
  if (template[prop] && !ext[prop])
1584
1580
  ext[prop] = Object.create( null );
1585
1581
  const location = weakRefLocation( ref );
1582
+
1583
+ // prevent recompilation issue when localized entity is included in aspect or
1584
+ // entity without keys (via shadowing):
1585
+ const checkForLocalized = prop === 'elements' &&
1586
+ template.kind === 'entity' && !template.query &&
1587
+ withLocalizedData( template, ext );
1588
+
1586
1589
  // eslint-disable-next-line no-loop-func
1587
1590
  forEachInOrder( template, prop, ( origin, name ) => {
1591
+ if (checkForLocalized && (name === 'texts' || name === 'localized'))
1592
+ return;
1588
1593
  if (members && members[name]) {
1589
1594
  if (!includesNonShadowedFirst && !ext[prop][name])
1590
1595
  dictAdd( ext[prop], name, members[name] ); // to keep order
@@ -1603,7 +1608,7 @@ function extend( model ) {
1603
1608
  if (origin.value && origin.$syntax === 'calc') {
1604
1609
  // TODO: If paths become invalid in the new artifact, should we mark
1605
1610
  // all usages in the expressions? Possibly just the first one?
1606
- // TODO: Unify with coding in extend.js
1611
+ // TODO: Unify with other code in extend.js
1607
1612
  elem.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
1608
1613
  elem.$syntax = 'calc';
1609
1614
  createAndLinkCalcDepElement( elem );
@@ -1630,6 +1635,31 @@ function extend( model ) {
1630
1635
  }
1631
1636
  }
1632
1637
 
1638
+ /**
1639
+ * Return true if include (with `name` and `elements`) in `ext` highly likely
1640
+ * contain elements `texts` and `localized` which are compiler-generated for
1641
+ * localized data. Due to recompilation, we cannot just check the `$inferred`
1642
+ * property in the XSN, but need to apply an heuristics.
1643
+ *
1644
+ * It is as follows: the elements `texts` and `localized` in entity `‹E›` and
1645
+ * the entity `‹E›.texts` are considered compiler-generated for Localized Data if
1646
+ *
1647
+ * - `‹E›.texts` has a key element `locale`
1648
+ * - `texts` of `‹E›` is an unmanaged composition to `‹E›.texts`
1649
+ * - `localized` of `‹E›` is an unmanaged association to `‹E›.texts`
1650
+ */
1651
+ function withLocalizedData( { name, elements }, ext ) {
1652
+ const textsEntityName = `${ name.id }.texts`;
1653
+ if (!model.definitions[textsEntityName]?.elements?.locale?.key?.val)
1654
+ return false;
1655
+ const { texts, localized } = elements ?? {};
1656
+ return texts?.target && localized?.target && texts.on && localized.on &&
1657
+ resolveUncheckedPath( texts.target, 'target', ext ) === textsEntityName &&
1658
+ resolveUncheckedPath( localized.target, 'target', ext ) === textsEntityName &&
1659
+ resolveUncheckedPath( texts.type, 'type', ext ) === 'cds.Composition' &&
1660
+ resolveUncheckedPath( localized.type, 'type', ext ) === 'cds.Association';
1661
+ }
1662
+
1633
1663
  /**
1634
1664
  * Report duplicates in parent[prop] that happen due to multiple includes having the
1635
1665
  * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
@@ -42,7 +42,7 @@ function finalizeParseCdl( model ) {
42
42
  for (const ext of late[name]._extensions) {
43
43
  ext.name.id = resolveUncheckedPath( ext.name, '_uncheckedExtension', ext );
44
44
  // Initialize members and define annotations in sub-elements.
45
- initMembers( ext, true );
45
+ initMembers( ext );
46
46
  extensions.push( ext );
47
47
  }
48
48
  }
@@ -31,7 +31,7 @@ function generate( model ) {
31
31
  const {
32
32
  resolvePath,
33
33
  resolveUncheckedPath,
34
- initArtifact,
34
+ initMainArtifact,
35
35
  extendArtifactBefore,
36
36
  applyIncludes,
37
37
  } = model.$functions;
@@ -173,8 +173,8 @@ function generate( model ) {
173
173
  const localized = localizedData( art, textsEntity, fioriEnabled );
174
174
  if (!localized)
175
175
  return;
176
- if (textsEntity) // expanded localized data in source
177
- return; // -> make it idempotent
176
+ if (textsEntity && textsEntity.kind !== 'namespace')
177
+ return; // expanded texts entity in source -> nothing to do
178
178
  createTextsEntity( art, textsName, localized, fioriEnabled );
179
179
  addTextsAssociations( art, textsName, localized );
180
180
  }
@@ -236,7 +236,10 @@ function generate( model ) {
236
236
  return false;
237
237
  }
238
238
 
239
- if (textsEntity) {
239
+ if (textsEntity?.kind === 'namespace') { // namespace Base.texts
240
+ textsEntity = null;
241
+ }
242
+ else if (textsEntity) {
240
243
  if (textsEntity.$duplicates)
241
244
  return false;
242
245
  if (textsEntity.kind !== 'entity' || textsEntity.query ||
@@ -283,15 +286,19 @@ function generate( model ) {
283
286
  */
284
287
  function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
285
288
  const location = weakLocation( base.elements[$location] || base.location );
286
- const art = {
289
+ let art = {
287
290
  kind: 'entity',
288
291
  name: { id: absolute, location },
289
292
  location,
290
293
  elements: Object.create( null ),
291
294
  $inferred: 'localized-entity',
292
295
  };
296
+ const gap = model.definitions[absolute];
297
+ if (gap)
298
+ art = Object.assign( gap, art );
299
+ else
300
+ model.definitions[absolute] = art;
293
301
  setLink( art, '_block', model.$internal );
294
- model.definitions[absolute] = art;
295
302
  extendArtifactBefore( art ); // having extensions here would be wrong
296
303
 
297
304
  if (!fioriEnabled) {
@@ -340,7 +347,12 @@ function generate( model ) {
340
347
  for (const orig of textElems)
341
348
  addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue );
342
349
 
343
- initArtifact( art );
350
+ initMainArtifact( art );
351
+ // do the kick-start relevant stuff: _service, there are no _ancestors,
352
+ // _descendants would have been set already for a gap artifact
353
+ setLink( art, '_service', art._parent._service );
354
+ model.$compositionTargets[absolute] = true;
355
+
344
356
  if (art.includes) {
345
357
  // add elements `locale`, etc. which are required below.
346
358
  applyIncludes( art, art ); // TODO: rethink - can we avoid this if only new extend?
@@ -641,7 +653,8 @@ function generate( model ) {
641
653
  'An aspect $(TARGET) with an element named $(NAME) can\'t be used as target' );
642
654
  return false;
643
655
  }
644
- if (model.definitions[entityName]) {
656
+ const place = model.definitions[entityName];
657
+ if (place && place.kind !== 'namespace') {
645
658
  error( null, [ location, elem ], { art: entityName },
646
659
  // eslint-disable-next-line @stylistic/max-len
647
660
  'Target entity $(ART) can\'t be created as there is another definition with this name' );
@@ -687,7 +700,7 @@ function generate( model ) {
687
700
  $inferred: 'aspect-composition',
688
701
  };
689
702
 
690
- const art = {
703
+ let art = {
691
704
  kind: 'entity',
692
705
  name: {
693
706
  id: entityName,
@@ -698,6 +711,12 @@ function generate( model ) {
698
711
  elements: Object.create( null ),
699
712
  $inferred: 'composition-entity',
700
713
  };
714
+ const gap = model.definitions[entityName];
715
+ if (gap)
716
+ art = Object.assign( gap, art );
717
+ else
718
+ model.definitions[entityName] = art;
719
+
701
720
  if (target.name) { // named target aspect
702
721
  if (!isDeprecatedEnabled( options, 'noCompositionIncludes' )) {
703
722
  art.includes = [ createInclude( target.name.id, location ) ];
@@ -741,8 +760,12 @@ function generate( model ) {
741
760
  addProxyElements( art, target.elements, 'aspect-composition', enforceLocation && location );
742
761
 
743
762
  setLink( art, '_block', model.$internal );
744
- model.definitions[entityName] = art;
745
- initArtifact( art );
763
+ initMainArtifact( art );
764
+
765
+ // do the kick-start relevant stuff: _service, there are no _ancestors,
766
+ // _descendants would have been set already for a gap artifact
767
+ setLink( art, '_service', art._parent._service );
768
+ model.$compositionTargets[entityName] = true;
746
769
 
747
770
  // Apply annotations to generated artifact, prepare (not apply!) element
748
771
  // annotations (remark: adding elements is not allowed for generated artifacts):
@@ -495,8 +495,8 @@ function compileDoXSync( model ) {
495
495
  return model;
496
496
  }
497
497
  extend( model );
498
- generate( model );
499
498
  kickStart( model );
499
+ generate( model );
500
500
  populate( model );
501
501
 
502
502
  model.definitions = model.$functions.shuffleDict( model.definitions );
@@ -546,7 +546,7 @@ async function compileDoX( model ) {
546
546
  return model;
547
547
  }
548
548
 
549
- for (const phase of [ extend, generate, kickStart, populate ]) {
549
+ for (const phase of [ extend, kickStart, generate, populate ]) {
550
550
  phase( model );
551
551
  // eslint-disable-next-line no-await-in-loop
552
552
  await checkAsyncAbortFlag( options.abortSignal );
@@ -2,6 +2,7 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ const { builtinLocation } = require('../base/location');
5
6
  const { isBetaEnabled, forEachGeneric } = require('../base/model');
6
7
  const {
7
8
  setLink,
@@ -14,14 +15,12 @@ function kickStart( model ) {
14
15
  const { options } = model;
15
16
  const { message } = model.$messageFunctions;
16
17
 
17
- const { resolveUncheckedPath, resolvePath } = model.$functions;
18
+ const { resolveUncheckedPath, initMainArtifact } = model.$functions;
18
19
 
19
20
  // Set _service link (sorted to set it on parent first). Could be set
20
21
  // directly, but beware a namespace becoming a service later.
21
22
  Object.keys( model.definitions ).sort().forEach( setAncestorsAndService );
22
23
  forEachGeneric( model, 'definitions', postProcessArtifact );
23
-
24
- forEachGeneric( model, 'sources', resolveUsings );
25
24
  return;
26
25
 
27
26
 
@@ -32,6 +31,9 @@ function kickStart( model ) {
32
31
  * - service: the artifact of the embedding service
33
32
  * This function must be called ordered: parent first
34
33
  *
34
+ * Remark: _service links are not already set in define.js, because we might
35
+ * have a @cds.redirection.service in the future
36
+ *
35
37
  * @param {string} name Artifact name
36
38
  */
37
39
  function setAncestorsAndService( name ) {
@@ -50,16 +52,12 @@ function kickStart( model ) {
50
52
  return;
51
53
  // To be removed when nested services are allowed
52
54
  if (!isBetaEnabled( options, 'nestedServices' ) && art.kind === 'service') {
53
- while (parent.kind !== 'service')
54
- parent = parent._parent;
55
- message( 'service-nested-service', [ art.name.location, art ], { art: parent },
55
+ message( 'service-nested-service', [ art.name.location, art ], { art: service },
56
56
  'A service can\'t be nested within a service $(ART)' );
57
57
  }
58
58
  else if (art.kind === 'context') {
59
- while (parent.kind !== 'service')
60
- parent = parent._parent;
61
59
  // TODO: remove this error
62
- message( 'service-nested-context', [ art.name.location, art ], { art: parent },
60
+ message( 'service-nested-context', [ art.name.location, art ], { art: service },
63
61
  'A context can\'t be nested within a service $(ART)' );
64
62
  }
65
63
  }
@@ -73,7 +71,9 @@ function kickStart( model ) {
73
71
  // Service1.E = projection on E
74
72
 
75
73
  // Remark: _ancestors are also set with includes, and there also for aspects,
76
- // types and events.
74
+ // types and events (TODO: entity only)
75
+ //
76
+ // Remark: _ancestors are also tested in populate.js for minmal exposure
77
77
  const chain = [];
78
78
  const autoexposed = annotationVal( art['@cds.autoexposed'] );
79
79
  // no need to set preferredRedirectionTarget in the while loop as we would
@@ -84,7 +84,7 @@ function kickStart( model ) {
84
84
  chain.push( art );
85
85
  setLink( art, '_ancestors', 0 ); // avoid infloop with cyclic from
86
86
  const name = resolveUncheckedPath( art.query.from, 'from', art );
87
- art = name && model.definitions[name];
87
+ art = name && (model.definitions[name] || createGapArtifact( name ));
88
88
  if (autoexposed)
89
89
  break; // only direct projection for auto-exposed
90
90
  }
@@ -97,6 +97,18 @@ function kickStart( model ) {
97
97
  }
98
98
  }
99
99
 
100
+ function createGapArtifact( name, location = builtinLocation() ) {
101
+ // TODO: make it probably part of define.js
102
+ // TODO: make it work without location (or value undefined/null)
103
+ // TODO: change the location later if overwritten
104
+ const art = {
105
+ kind: 'namespace', name: { id: name, location }, location,
106
+ };
107
+ model.definitions[name] = art;
108
+ initMainArtifact( art );
109
+ return art;
110
+ }
111
+
100
112
  function postProcessArtifact( art ) {
101
113
  tagCompositionTargets( art );
102
114
  if (art.$queries) {
@@ -127,40 +139,19 @@ function kickStart( model ) {
127
139
  }
128
140
 
129
141
  function tagCompositionTargets( elem ) {
142
+ // TODO: together with test for targetIsTargetAspect()
130
143
  if (elem.target && isDirectComposition( elem )) {
131
144
  // A target aspect would have already moved to property `targetAspect` in
132
145
  // define.js (hm... more something for kick-start.js...)
133
146
  // TODO: for safety, just use resolveUncheckedPath()
134
- const target = resolvePath( elem.target, 'target', elem );
147
+ const target = resolveUncheckedPath( elem.target, 'target', elem );
135
148
  if (target)
136
- model.$compositionTargets[target.name.id] = true;
149
+ model.$compositionTargets[target] = true;
137
150
  }
138
151
  if (elem.targetAspect?.elements)
139
152
  elem = elem.targetAspect;
140
153
  forEachGeneric( elem, 'elements', tagCompositionTargets );
141
154
  }
142
-
143
- // Resolve the using declarations in `using`. Issue
144
- // error message if the referenced artifact does not exist.
145
- // TODO: think of moving this to resolve.js
146
- function resolveUsings( src, topLevel ) {
147
- if (!src.usings)
148
- return;
149
- for (const def of src.usings) {
150
- if (def.usings) // using {...}
151
- resolveUsings( def );
152
- if (!def.name || !def.name.id)
153
- continue; // using {...}, parse error
154
- const art = model.definitions[def.name.absolute];
155
- if (art && art.$duplicates)
156
- continue;
157
- const ref = def.extern;
158
- const user = (topLevel ? def : src);
159
- const from = user.fileDep;
160
- if (art || !from || from.realname) // no error for non-existing ref with non-existing module
161
- resolvePath( ref, 'using', def ); // TODO: consider FROM for validNames
162
- }
163
- }
164
155
  }
165
156
 
166
157
  module.exports = kickStart;
@@ -75,7 +75,7 @@ function populate( model ) {
75
75
  resolvePath,
76
76
  nestedElements,
77
77
  attachAndEmitValidNames,
78
- initArtifact,
78
+ initMainArtifact,
79
79
  extendArtifactBefore,
80
80
  extendArtifactAfter,
81
81
  } = model.$functions;
@@ -1113,9 +1113,7 @@ function populate( model ) {
1113
1113
  // To avoid repeated messages: if already tried to do autoexposure, return
1114
1114
  // auto-exposed entity when successful, or `target` otherwise (no/failed autoexposure)
1115
1115
  function minimalExposure( target, service, elemScope ) {
1116
- const descendants = scopedExposure( target._descendants &&
1117
- target._descendants[service.name.id] ||
1118
- [],
1116
+ const descendants = scopedExposure( target._descendants?.[service.name.id] || [],
1119
1117
  elemScope, target );
1120
1118
  const preferred = descendants.filter( d => annotationVal( d['@cds.redirection.target'] ) );
1121
1119
  const exposed = preferred.length ? preferred : descendants;
@@ -1172,7 +1170,7 @@ function populate( model ) {
1172
1170
  kind: 'namespace', name: { id: autoScopeName, location }, location,
1173
1171
  };
1174
1172
  model.definitions[autoScopeName] = nullScope;
1175
- initArtifact( nullScope );
1173
+ initMainArtifact( nullScope );
1176
1174
  return nullScope;
1177
1175
  }
1178
1176
 
@@ -1207,7 +1205,6 @@ function populate( model ) {
1207
1205
  function isDirectProjection( proj, base ) {
1208
1206
  return proj.kind === 'entity' && // not event
1209
1207
  // direct proj (TODO: or should we add them to another list?)
1210
- // TODO: delete ENTITY._from - maybe not...
1211
1208
  proj.query && proj.query.op && proj.query.op.val === 'SELECT' &&
1212
1209
  proj._from && proj._from.length === 1 &&
1213
1210
  base === resolvePath( proj._from[0], 'from', proj.query );
@@ -1354,7 +1351,7 @@ function populate( model ) {
1354
1351
  }
1355
1352
  setLink( art, '_service', service );
1356
1353
  setLink( art, '_block', model.$internal );
1357
- initArtifact( art, !!autoexposed );
1354
+ initMainArtifact( art, !!autoexposed );
1358
1355
  effectiveType( art );
1359
1356
  // TODO: try to set locations of elements locations of orig target elements
1360
1357
  newAutoExposed.push( art );
@@ -47,7 +47,7 @@ function propagate( model ) {
47
47
  precision: always,
48
48
  scale: always,
49
49
  srid: always,
50
- localized: withKind,
50
+ localized,
51
51
  target: notWithExpand,
52
52
  targetAspect,
53
53
  cardinality: notWithExpand,
@@ -78,6 +78,7 @@ function propagate( model ) {
78
78
  const { rewriteAnnotationsRefs, rewriteRefsInExpression } = xprRewriteFns( model );
79
79
 
80
80
  const { message, throwWithError } = model.$messageFunctions;
81
+ const { withLocalizedData } = model.$functions;
81
82
 
82
83
  forEachDefinition( model, run );
83
84
  forEachGeneric( model, 'vocabularies', run );
@@ -336,6 +337,24 @@ function propagate( model ) {
336
337
  annotation( prop, target, source );
337
338
  }
338
339
 
340
+ /**
341
+ * Propagate `localized`, but not for a texts entity
342
+ * (as `null` in an Universal CSN).
343
+ */
344
+ function localized( prop, target, source ) {
345
+ const main = target._main;
346
+ if (target.kind === 'element' && main.kind === 'entity' && !main.query) {
347
+ const { id } = main.name;
348
+ const base = id.endsWith( '.texts' ) &&
349
+ model.definitions[id.slice( 0, -'.texts'.length )];
350
+ if (base && withLocalizedData( base, main )) {
351
+ target.localized = { $inferred: 'NULL', val: undefined }; // null in UCSN
352
+ return;
353
+ }
354
+ }
355
+ withKind( prop, target, source );
356
+ }
357
+
339
358
  function withKind( prop, target, source ) {
340
359
  if (target.kind === 'param' && source.kind === 'entity')
341
360
  return; // Don't propagate from entity types to parameters (+ return type).
@@ -108,8 +108,32 @@ function resolve( model ) {
108
108
  const ignoreSpecifiedElements
109
109
  = isDeprecatedEnabled( options, 'ignoreSpecifiedQueryElements' );
110
110
 
111
+ forEachGeneric( model, 'sources', resolveUsings );
111
112
  return doResolve();
112
113
 
114
+ /**
115
+ * Resolve the using declarations in `using`.
116
+ * Issue error message if the referenced artifact does not exist.
117
+ */
118
+ function resolveUsings( src, topLevel ) {
119
+ if (!src.usings)
120
+ return;
121
+ for (const def of src.usings) {
122
+ if (def.usings) // using {...}
123
+ resolveUsings( def );
124
+ if (!def.name || !def.name.id)
125
+ continue; // using {...}, parse error
126
+ const art = model.definitions[def.name.absolute];
127
+ if (art && art.$duplicates)
128
+ continue;
129
+ const ref = def.extern;
130
+ const user = (topLevel ? def : src);
131
+ const from = user.fileDep;
132
+ if (art || !from || from.realname) // no error for non-existing ref with non-existing module
133
+ resolvePath( ref, 'using', def ); // TODO: consider FROM for validNames
134
+ }
135
+ }
136
+
113
137
  function doResolve() {
114
138
  // Phase 1: check paths in `usings` has been moved to kick-start.js Phase 2:
115
139
  // calculate/init view elements & collect views in order:
@@ -1669,7 +1693,8 @@ function resolve( model ) {
1669
1693
  }
1670
1694
  const symbols = type && type.enum;
1671
1695
  if (!symbols) {
1672
- if (user.kind !== '$annotation') { // TODO: better type deduction for annotations
1696
+ if ((user.kind ?? user._outer?.kind) !== '$annotation') {
1697
+ // TODO: better type deduction for annotations
1673
1698
  const msg = (user.kind === 'enum') ? 'symbolDef' : type && 'invalidType';
1674
1699
  warning( 'ref-unexpected-enum', [ expr.location, user ],
1675
1700
  { '#': msg || 'untyped', enum: sym.id, type: type || '' } );