@sap/cds-compiler 3.5.4 → 3.6.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 (84) hide show
  1. package/CHANGELOG.md +65 -2
  2. package/bin/cdsc.js +14 -6
  3. package/doc/CHANGELOG_ARCHIVE.md +10 -10
  4. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  5. package/lib/api/main.js +32 -55
  6. package/lib/api/options.js +3 -2
  7. package/lib/api/validate.js +5 -0
  8. package/lib/base/message-registry.js +104 -32
  9. package/lib/base/messages.js +277 -212
  10. package/lib/base/optionProcessorHelper.js +9 -2
  11. package/lib/base/shuffle.js +50 -0
  12. package/lib/checks/actionsFunctions.js +37 -20
  13. package/lib/checks/foreignKeys.js +13 -6
  14. package/lib/checks/nonexpandableStructured.js +1 -2
  15. package/lib/checks/onConditions.js +21 -19
  16. package/lib/checks/parameters.js +1 -1
  17. package/lib/checks/queryNoDbArtifacts.js +2 -0
  18. package/lib/checks/types.js +16 -22
  19. package/lib/compiler/assert-consistency.js +31 -28
  20. package/lib/compiler/builtins.js +20 -4
  21. package/lib/compiler/checks.js +72 -63
  22. package/lib/compiler/define.js +396 -314
  23. package/lib/compiler/extend.js +55 -49
  24. package/lib/compiler/index.js +5 -0
  25. package/lib/compiler/populate.js +28 -11
  26. package/lib/compiler/propagator.js +2 -1
  27. package/lib/compiler/resolve.js +28 -13
  28. package/lib/compiler/shared.js +15 -10
  29. package/lib/compiler/utils.js +7 -7
  30. package/lib/edm/annotations/genericTranslation.js +51 -46
  31. package/lib/edm/annotations/preprocessAnnotations.js +37 -40
  32. package/lib/edm/csn2edm.js +69 -21
  33. package/lib/edm/edm.js +2 -2
  34. package/lib/edm/edmInboundChecks.js +6 -8
  35. package/lib/edm/edmPreprocessor.js +88 -80
  36. package/lib/edm/edmUtils.js +6 -15
  37. package/lib/gen/Dictionary.json +81 -13
  38. package/lib/gen/language.checksum +1 -1
  39. package/lib/gen/language.interp +2 -1
  40. package/lib/gen/languageParser.js +4680 -4484
  41. package/lib/inspect/inspectModelStatistics.js +2 -1
  42. package/lib/inspect/inspectPropagation.js +2 -1
  43. package/lib/json/from-csn.js +131 -78
  44. package/lib/json/to-csn.js +39 -23
  45. package/lib/language/antlrParser.js +0 -3
  46. package/lib/language/docCommentParser.js +7 -3
  47. package/lib/language/errorStrategy.js +3 -2
  48. package/lib/language/genericAntlrParser.js +96 -41
  49. package/lib/language/language.g4 +112 -128
  50. package/lib/language/multiLineStringParser.js +2 -1
  51. package/lib/main.d.ts +115 -2
  52. package/lib/main.js +16 -3
  53. package/lib/model/csnRefs.js +32 -4
  54. package/lib/model/csnUtils.js +109 -179
  55. package/lib/model/enrichCsn.js +13 -8
  56. package/lib/model/revealInternalProperties.js +4 -3
  57. package/lib/optionProcessor.js +22 -3
  58. package/lib/render/manageConstraints.js +11 -15
  59. package/lib/render/toCdl.js +144 -47
  60. package/lib/render/toHdbcds.js +22 -22
  61. package/lib/render/toRename.js +3 -4
  62. package/lib/render/toSql.js +31 -22
  63. package/lib/render/utils/delta.js +3 -1
  64. package/lib/render/utils/sql.js +2 -14
  65. package/lib/transform/db/associations.js +6 -6
  66. package/lib/transform/db/cdsPersistence.js +3 -3
  67. package/lib/transform/db/constraints.js +4 -6
  68. package/lib/transform/db/expansion.js +4 -4
  69. package/lib/transform/db/flattening.js +12 -15
  70. package/lib/transform/db/temporal.js +4 -3
  71. package/lib/transform/db/transformExists.js +13 -7
  72. package/lib/transform/draft/db.js +7 -7
  73. package/lib/transform/forOdataNew.js +15 -4
  74. package/lib/transform/forRelationalDB.js +59 -41
  75. package/lib/transform/odata/toFinalBaseType.js +106 -82
  76. package/lib/transform/odata/typesExposure.js +26 -17
  77. package/lib/transform/odata/utils.js +1 -1
  78. package/lib/transform/parseExpr.js +1 -1
  79. package/lib/transform/transformUtilsNew.js +33 -10
  80. package/lib/transform/translateAssocsToJoins.js +8 -7
  81. package/lib/transform/universalCsn/coreComputed.js +7 -5
  82. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -4
  83. package/lib/utils/timetrace.js +2 -2
  84. package/package.json +1 -2
@@ -54,7 +54,7 @@ function extend( model ) {
54
54
  applyExtensions();
55
55
 
56
56
  const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
57
- const useTextsAspects = checkTextsAspects();
57
+ const useTextsAspect = checkTextsAspect();
58
58
 
59
59
  Object.keys( model.definitions ).forEach( processArtifact );
60
60
 
@@ -301,38 +301,24 @@ function extend( model ) {
301
301
  *
302
302
  * @return {boolean}
303
303
  */
304
- function checkTextsAspects() {
304
+ function checkTextsAspect() {
305
305
  const textsAspect = model.definitions['sap.common.TextsAspect'];
306
- const fioriTextsAspect = model.definitions['sap.common.FioriTextsAspect'];
307
-
308
- let hasError = false;
309
-
310
- if (textsAspect) {
311
- const specialElements = { locale: { key: true } };
312
- if (!checkTextsAspect(textsAspect, specialElements))
313
- hasError = true;
314
- }
315
-
316
- if (fioriTextsAspect) {
317
- const specialElements = { ID_texts: { key: true }, locale: { key: false } };
318
- if (!checkTextsAspect(fioriTextsAspect, specialElements))
319
- hasError = true;
320
- }
306
+ if (!textsAspect)
307
+ return false;
321
308
 
322
- return !hasError;
323
- }
309
+ const specialElements = { locale: { key: true } };
324
310
 
325
- function checkTextsAspect( art, specialElements ) {
326
- if (art.kind !== 'aspect' || !art.elements) {
327
- error('def-invalid-texts-aspect', [ art.name.location, art ], { '#': 'no-aspect', art });
311
+ if (textsAspect.kind !== 'aspect' || !textsAspect.elements) {
312
+ error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
313
+ { '#': 'no-aspect', art: textsAspect });
328
314
  return false;
329
315
  }
330
316
 
331
317
  let hasError = false;
332
- if (addTextsLanguageAssoc && art.elements.language) {
333
- const lang = art.elements.language;
318
+ if (addTextsLanguageAssoc && textsAspect.elements.language) {
319
+ const lang = textsAspect.elements.language;
334
320
  error('def-unexpected-element', [ lang.name.location, lang ],
335
- { option: 'addTextsLanguageAssoc', art, name: 'language' },
321
+ { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
336
322
  // eslint-disable-next-line max-len
337
323
  '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
338
324
  hasError = true;
@@ -340,14 +326,14 @@ function extend( model ) {
340
326
 
341
327
  for (const name in specialElements) {
342
328
  const expected = specialElements[name];
343
- const elem = art.elements[name];
329
+ const elem = textsAspect.elements[name];
344
330
  if (!elem) {
345
- error('def-invalid-texts-aspect', [ art.name.location, art ],
346
- { '#': 'missing', art, name });
331
+ error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
332
+ { '#': 'missing', art: textsAspect, name });
347
333
  hasError = true;
348
334
  }
349
335
  else if (expected.key !== undefined && !!elem.key?.val !== expected.key) {
350
- const loc = elem.key?.location || elem.name?.location || art.name.location;
336
+ const loc = elem.key?.location || elem.name?.location || textsAspect.name.location;
351
337
  error('def-invalid-texts-aspect', [ loc, elem ],
352
338
  { '#': expected.key ? 'key' : 'no-key', art: elem });
353
339
  hasError = true;
@@ -357,13 +343,13 @@ function extend( model ) {
357
343
  if (hasError) // avoid subsequent errors, if the special elements are already wrong
358
344
  return false;
359
345
 
360
- for (const name in art.elements) {
361
- const elem = art.elements[name];
346
+ for (const name in textsAspect.elements) {
347
+ const elem = textsAspect.elements[name];
362
348
  const include = elem.$inferred === 'include';
363
349
  if (!specialElements[name] && elem.key) {
364
350
  const loc = include ? elem.location : elem.key.location;
365
351
  error( 'def-unexpected-key', [ loc, elem ],
366
- { '#': !include ? 'std' : 'include', art } );
352
+ { '#': !include ? 'std' : 'include', art: textsAspect } );
367
353
  hasError = true;
368
354
  }
369
355
  else if (hasTruthyProp( elem, 'localized' )) {
@@ -371,11 +357,12 @@ function extend( model ) {
371
357
  // Not supported anyway, but important for recompilation (which fails correctly).
372
358
  const loc = elem.localized?.location || elem.location;
373
359
  error( 'def-unexpected-localized', [ loc, elem ],
374
- { '#': !include ? 'std' : 'include', art } );
360
+ { '#': !include ? 'std' : 'include', art: textsAspect } );
375
361
  hasError = true;
376
362
  }
377
363
  else if (elem.targetAspect) {
378
- error( 'def-unexpected-composition', [ elem.targetAspect.location, elem ], { art },
364
+ error( 'def-unexpected-composition', [ elem.targetAspect.location, elem ],
365
+ { art: textsAspect },
379
366
  '$(ART) can\'t have composition of aspects' );
380
367
  hasError = true;
381
368
  }
@@ -607,7 +594,7 @@ function extend( model ) {
607
594
  for (const ext of exts)
608
595
  info( 'anno-builtin', [ ext.name.location, ext ] );
609
596
  }
610
- // created texts entity, autoexposed entity
597
+ // created texts entity, auto-exposed entity
611
598
  if (exts) {
612
599
  extendArtifact( exts, art, 'gen' );
613
600
  if (veryLate)
@@ -728,13 +715,12 @@ function extend( model ) {
728
715
  }
729
716
 
730
717
  /**
731
- * Add all members (e.g. elements or actions) of `ext.includes` to `ext[prop]`
732
- * if `parent` is `false` or to `parent` with appropriate _origin links otherwise.
733
- * Included members are prepended to existing ones.
718
+ * Add all members (e.g. elements or actions) of `ext.includes` to `ext[prop]`.
719
+ * If `art` is `ext`, set the parent link accordingly.
734
720
  *
735
721
  * @param {XSN.Extension} ext
736
722
  * @param {XSN.Artifact} art
737
- * @param {string} prop
723
+ * @param {string} prop: 'elements' or 'actions'
738
724
  */
739
725
  function includeMembers( ext, art, prop ) {
740
726
  // TODO two kind of messages:
@@ -892,10 +878,7 @@ function extend( model ) {
892
878
  * @param {boolean} fioriEnabled
893
879
  */
894
880
  function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
895
- const name = (fioriEnabled ? 'sap.common.FioriTextsAspect' : 'sap.common.TextsAspect');
896
- const withTextsAspect = useTextsAspects && model.definitions[name];
897
-
898
- const art = withTextsAspect
881
+ const art = useTextsAspect
899
882
  ? createTextsEntityWithInclude( base, absolute, fioriEnabled )
900
883
  : createTextsEntityWithDefaultElements( base, absolute, fioriEnabled );
901
884
 
@@ -955,7 +938,16 @@ function extend( model ) {
955
938
  applyIncludes(art, art);
956
939
  }
957
940
 
958
- if (fioriEnabled ) {
941
+ if (fioriEnabled) {
942
+ // The includes mechanism puts TextsAspect's elements before .texts' elements.
943
+ // Because ID_texts is not copied from TextsAspect, the order is messed
944
+ // up. Fix it.
945
+ const { elements } = art;
946
+ art.elements = Object.create(null);
947
+ const names = [ 'ID_texts', 'locale', ...Object.keys(elements) ];
948
+ for (const name of names)
949
+ art.elements[name] = elements[name];
950
+
959
951
  const { locale } = art.elements;
960
952
  assertUniqueValue.unshift({
961
953
  path: [ { id: locale.name.id, location: locale.location } ],
@@ -975,21 +967,19 @@ function extend( model ) {
975
967
  *
976
968
  * Does NOT apply the include!
977
969
  *
978
- * TODO: When beta flag textsAspect is removed, update caller-site and remove old coding.
979
- *
980
970
  * @param {XSN.Artifact} base
981
971
  * @param {string} absolute
982
972
  * @param {boolean} fioriEnabled
983
973
  */
984
974
  function createTextsEntityWithInclude( base, absolute, fioriEnabled ) {
975
+ const textsAspectName = 'sap.common.TextsAspect';
976
+ const textsAspect = model.definitions['sap.common.TextsAspect'];
985
977
  const elements = Object.create(null);
986
978
  const { location } = base.name;
987
-
988
- const include = fioriEnabled ? 'sap.common.FioriTextsAspect' : 'sap.common.TextsAspect';
989
979
  const art = {
990
980
  kind: 'entity',
991
981
  name: { path: splitIntoPath( location, absolute ), absolute, location },
992
- includes: [ createInclude( include, base.location ) ],
982
+ includes: [ createInclude( textsAspectName, base.location ) ],
993
983
  location: base.location,
994
984
  elements,
995
985
  $inferred: 'localized-entity',
@@ -1000,6 +990,22 @@ function extend( model ) {
1000
990
  // TODO (next major version): remove?
1001
991
  annotateWith( art, '@odata.draft.enabled', art.location, false );
1002
992
  }
993
+ else {
994
+ // @fiori.draft.enabled artifacts need default elements ID_texts and locale.
995
+ // `locale` is copied from `sap.common.TextsAspect`, but without "key".
996
+ const textId = {
997
+ name: { location, id: 'ID_texts' },
998
+ kind: 'element',
999
+ key: { val: true, location },
1000
+ type: augmentPath( location, 'cds.UUID' ),
1001
+ location,
1002
+ };
1003
+ dictAdd( art.elements, 'ID_texts', textId );
1004
+
1005
+ // "Early" include; only for element `locale`, which has its `key` property
1006
+ // removed (or rather: it is not copied).
1007
+ linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location );
1008
+ }
1003
1009
 
1004
1010
  if (addTextsLanguageAssoc && art.elements.language)
1005
1011
  art.elements.language = undefined; // TODO: Message? Ignore?
@@ -465,6 +465,11 @@ function compileDoX( model ) {
465
465
  extend( model );
466
466
  kickStart( model );
467
467
  populate( model );
468
+
469
+ model.definitions = model.$functions.shuffleDict( model.definitions );
470
+ // Shuffling extensions is more difficult due to intra-file extensions of same artifact
471
+ // TODO: think about making this work
472
+
468
473
  resolve( model );
469
474
  tweakAssocs( model );
470
475
  assertConsistency( model );
@@ -219,7 +219,7 @@ function populate( model ) {
219
219
  // console.log( 'EXPR-IN', art.kind, refString(art.name) )
220
220
  if (!art._main || !art.value || !art.value.path)
221
221
  return undefined;
222
- if (art._pathHead && art.value.path) {
222
+ if (art.value.path) {
223
223
  setLink( art, '_origin', resolvePath( art.value, 'expr', art, null ) );
224
224
  return art._origin;
225
225
  }
@@ -432,7 +432,8 @@ function populate( model ) {
432
432
  }
433
433
  }
434
434
  // console.log( resolveChain.map( v => msgName(v)+v._status ) );
435
- for (const view of resolveChain.reverse()) {
435
+ resolveChain.reverse();
436
+ for (const view of resolveChain) {
436
437
  if (view._status !== '_query' ) { // not already resolved
437
438
  setLink( view, '_status', '_query' );
438
439
  // must be run in order “sub query in FROM first”:
@@ -454,11 +455,11 @@ function populate( model ) {
454
455
  *
455
456
  * This is important to ensure re-compilability.
456
457
  *
458
+ * TODO: make this part of a revamped on-demand 'extend' functionality.
459
+ *
457
460
  * @param art
458
461
  */
459
462
  function mergeSpecifiedElementsOrEnum( art ) {
460
- // Later we use specified elements as proxies to inferred of leading query
461
- // (No, we probably do not.)
462
463
  for (const id in (art.elements || art.enum)) {
463
464
  const ielem = art.elements ? art.elements[id] : art.enum[id]; // inferred element
464
465
  const selem = art.elements$ ? art.elements$[id] : art.enum$[id]; // specified element
@@ -807,15 +808,16 @@ function populate( model ) {
807
808
  const { id } = sibling.name;
808
809
  if (Array.isArray(navElem)) {
809
810
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
810
- info( 'wildcard-excluding-many', [ sibling.name.location, query ], { id },
811
- // eslint-disable-next-line max-len
811
+ info( 'wildcard-excluding-many', [ sibling.name.location, query ],
812
+ { id, keyword: 'excluding' },
813
+ // eslint-disable-next-line max-len
812
814
  'This select item replaces $(ID) from two or more sources. Add $(ID) to $(KEYWORD) to silence this message' );
813
815
  }
814
816
  else {
815
817
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
816
818
  info( 'wildcard-excluding-one', [ sibling.name.location, query ],
817
819
  { id, alias: navElem._parent.name.id, keyword: 'excluding' },
818
- // eslint-disable-next-line max-len
820
+ // eslint-disable-next-line max-len
819
821
  'This select item replaces $(ID) from table alias $(ALIAS). Add $(ID) to $(KEYWORD) to silence this message' );
820
822
  }
821
823
  }
@@ -1001,7 +1003,7 @@ function populate( model ) {
1001
1003
  // - exclude all indirect projections, i.e. those which are projection on others in list
1002
1004
  //
1003
1005
  // To avoid repeated messages: if already tried to do autoexposure, return
1004
- // autoexposed entity when successful, or `target` otherwise (no/failed autoexposure)
1006
+ // auto-exposed entity when successful, or `target` otherwise (no/failed autoexposure)
1005
1007
  function minimalExposure( target, service, elemScope ) {
1006
1008
  const descendants = scopedExposure( target._descendants &&
1007
1009
  target._descendants[service.name.absolute] ||
@@ -1175,7 +1177,7 @@ function populate( model ) {
1175
1177
  if (annotationIsFalse( anno )) {
1176
1178
  // It would probably be cleaner to ignore a dubious
1177
1179
  // `@cds.redirection.target: false` earlier, but that is not easy to detect
1178
- // due to the name of the autoexposed entity with scoped redirections
1180
+ // due to the name of the auto-exposed entity with scoped redirections
1179
1181
  if (!anno.$errorReported) {
1180
1182
  info( 'anno-redirecting-anyway',
1181
1183
  [ annotationLocation( anno ), autoexposed ],
@@ -1194,7 +1196,7 @@ function populate( model ) {
1194
1196
  }
1195
1197
  error( 'duplicate-autoexposed', [ service.name.location, service ],
1196
1198
  { target, art: absolute },
1197
- 'Name $(ART) of autoexposed entity for $(TARGET) collides with other definition' );
1199
+ 'Name $(ART) of auto-exposed entity for $(TARGET) collides with other definition' );
1198
1200
  info( null, [ target.name.location, target ],
1199
1201
  { art: service },
1200
1202
  'Expose this (or the competing) entity explicitly in service $(ART)' );
@@ -1203,7 +1205,7 @@ function populate( model ) {
1203
1205
  const firstTarget = autoexposed.query.from._artifact;
1204
1206
  error( 'duplicate-autoexposed', [ service.name.location, service ],
1205
1207
  { target: firstTarget, art: absolute },
1206
- 'Name $(ART) of autoexposed entity for $(TARGET) collides with other definition' );
1208
+ 'Name $(ART) of auto-exposed entity for $(TARGET) collides with other definition' );
1207
1209
  info( null, [ firstTarget.name.location, firstTarget ],
1208
1210
  { art: service },
1209
1211
  'Expose this (or the competing) entity explicitly in service $(ART)' );
@@ -1225,6 +1227,21 @@ function populate( model ) {
1225
1227
  $inferred: '$generated',
1226
1228
  },
1227
1229
  };
1230
+ // forward target parameters to projection
1231
+ if (target.params) {
1232
+ art.params = Object.create(null);
1233
+ // is art.query.from.path[0].$syntax: ':' required?
1234
+ art.query.from.path[0].args = Object.create(null);
1235
+ forEachGeneric(target, 'params', (p, pn) => {
1236
+ art.params[pn] = linkToOrigin(p, pn, art, 'params', p.location);
1237
+ art.query.from.path[0].args[pn] = {
1238
+ name: { id: p.name.id, location: p.location },
1239
+ location: p.location,
1240
+ scope: 'param',
1241
+ path: [ { id: pn, location: p.location } ],
1242
+ };
1243
+ });
1244
+ }
1228
1245
  // TODO: do we need to tag the generated entity with elemScope = 'auto'?
1229
1246
  if (autoexposed) {
1230
1247
  Object.assign( autoexposed, art );
@@ -111,7 +111,8 @@ function propagate( model ) {
111
111
  targets = news;
112
112
  }
113
113
 
114
- chain.reverse().forEach( step );
114
+ chain.reverse();
115
+ chain.forEach( step );
115
116
  runMembers( art );
116
117
  // console.log('DONE:', art.name, art.elements ? Object.keys(art.elements) : 0);
117
118
  }
@@ -69,6 +69,7 @@ const {
69
69
  } = require('./utils');
70
70
 
71
71
  const detectCycles = require('./cycle-detector');
72
+ const { CompilerAssertion } = require('../base/error');
72
73
 
73
74
  const $location = Symbol.for('cds.$location');
74
75
 
@@ -128,7 +129,7 @@ function resolve( model ) {
128
129
  // for builtin types
129
130
  forEachGeneric( model.definitions.cds, '_subArtifacts', chooseAnnotationsInArtifact);
130
131
  forEachGeneric( model.definitions['cds.hana'], '_subArtifacts', chooseAnnotationsInArtifact);
131
- // Phase 6: apply ANNOTATE on autoexposed entities and unknown artifacts:
132
+ // Phase 6: apply ANNOTATE on auto-exposed entities and unknown artifacts:
132
133
  lateExtensions( annotateMembers );
133
134
  if (model.extensions)
134
135
  model.extensions.map( annotateUnknown );
@@ -241,16 +242,16 @@ function resolve( model ) {
241
242
  for (const name in query.elements) {
242
243
  const elem = query.elements[name];
243
244
  if (!elem.$inferred && elem.value &&
244
- testExpr( elem.value, selectTest, () => false ))
245
+ testExpr( elem.value, selectTest, () => false, elem ))
245
246
  propagateKeys = false;
246
247
  }
247
248
  return propagateKeys;
248
249
 
249
- function selectTest( expr ) {
250
+ function selectTest( expr, user ) {
250
251
  const art = withAssociation( expr, targetMaxNotOne );
251
252
  if (art) {
252
253
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
253
- info( 'query-navigate-many', [ art.location, query ], { art },
254
+ info( 'query-navigate-many', [ art.location, user || query ], { art },
254
255
  {
255
256
  // eslint-disable-next-line max-len
256
257
  std: 'Navigating along to-many association $(ART) - key properties are not propagated',
@@ -290,10 +291,12 @@ function resolve( model ) {
290
291
  // all sub elements.
291
292
  // TODO: make this function smaller
292
293
  function resolveRefs( art ) {
294
+ if (art.builtin)
295
+ return;
293
296
  // console.log(message( null, art.location, art, {}, 'Info','REFS').toString())
294
297
  // console.log(message( null, art.location, art, {target:art.target}, 'Info','RR').toString())
295
298
  const parent = art._parent;
296
- const allowedInMain = [ 'entity', 'aspect' ].includes( adHocOrMainKind( art ) );
299
+ const allowedInMain = [ 'entity', 'aspect', 'event' ].includes( adHocOrMainKind( art ) );
297
300
  const isTopLevelElement = parent && (parent.kind !== 'element' || parent.targetAspect);
298
301
  if (art.key && art.key.val && !art.key.$inferred && !(allowedInMain && isTopLevelElement)) {
299
302
  warning( 'unexpected-key', [ art.key.location, art ],
@@ -384,6 +387,7 @@ function resolve( model ) {
384
387
  resolveRedirected( obj, obj.target._artifact );
385
388
  }
386
389
  else if (obj.kind === 'mixin') {
390
+ // TODO: also check that the type is cds.Association or cds.Composition
387
391
  error( 'non-assoc-in-mixin', [ (obj.type || obj.name).location, art ], {},
388
392
  'Only unmanaged associations are allowed in mixin clauses' );
389
393
  }
@@ -539,8 +543,14 @@ function resolve( model ) {
539
543
  'Elements only exist in entities, types or typed constructs' );
540
544
  }
541
545
  else {
546
+ const isEntity = (parent.returns?.type || parent.type)?._artifact?.kind === 'entity';
547
+ let variant = parent.enum ? 'enum' : 'element';
548
+ if (isEntity)
549
+ variant = 'entity-element';
550
+ else if (parent.returns)
551
+ variant = 'returns';
542
552
  notFound( 'anno-undefined-element', ext.name.location, art,
543
- { art: searchName( parent, name, parent.enum ? 'enum' : 'element' ) },
553
+ { '#': variant, art: parent, name },
544
554
  parent.elements || parent.enum );
545
555
  }
546
556
  }
@@ -688,7 +698,7 @@ function resolve( model ) {
688
698
  // enums would be first in elements
689
699
  if ( parent[kindProperties[kind].dict] &&
690
700
  parent[kindProperties[kind].dict][art.name.id] )
691
- throw new Error(art.name.id);
701
+ throw new CompilerAssertion(art.name.id);
692
702
  setMemberParent( ext, art.name.id, parent, kindProperties[kind].dict );
693
703
  }
694
704
  ext.kind = 'annotate'; // after setMemberParent()!
@@ -710,6 +720,8 @@ function resolve( model ) {
710
720
  if (prop.charAt(0) === '@')
711
721
  chooseAssignment( prop, art );
712
722
  }
723
+ if (art.doc)
724
+ chooseAssignment( 'doc', art );
713
725
  }
714
726
 
715
727
  function chooseAssignment( annoName, art ) {
@@ -741,9 +753,12 @@ function resolve( model ) {
741
753
  const msg = (issue === true)
742
754
  ? 'anno-duplicate'
743
755
  : (index >= 0) ? 'anno-duplicate-unrelated-layer' : 'anno-unstable-array';
756
+ const variant = annoName === 'doc' ? 'doc' : 'std';
744
757
  for (const a of assignments) {
745
- if (!a.$errorReported)
746
- message( msg, [ a.name.location, art ], { anno: annoName } );
758
+ if (!a.$errorReported) {
759
+ message( msg, [ a.name?.location || a.location, art ],
760
+ { '#': variant, anno: annoName } );
761
+ }
747
762
  }
748
763
  }
749
764
  // else if (index > 0) -- if we allow multiple assignments in one file - the last wins
@@ -877,7 +892,7 @@ function resolve( model ) {
877
892
  for (const col of query.$inlines)
878
893
  resolveExpr( col.value, 'expr', col, undefined, true );
879
894
  // for (const col of query.$inlines)
880
- // if (!col.value.path) throw Error(col.name.element)
895
+ // if (!col.value.path) throw new CompilerAssertion(col.name.element)
881
896
  if (query !== query._main._leadingQuery) // will be done later
882
897
  forEachGeneric( query, 'elements', resolveRefs );
883
898
  if (query.from)
@@ -1067,8 +1082,7 @@ function resolve( model ) {
1067
1082
  return;
1068
1083
  }
1069
1084
  else if ((elem.value || elem.expand) && elem.type && !elem.type.$inferred) {
1070
- error( 'ref-unexpected-assoc', [ elem.type.location, elem ], {},
1071
- 'Casting to an association is not supported' );
1085
+ error( 'ref-unexpected-assoc', [ elem.type.location, elem ], { '#': 'cast' } );
1072
1086
  return;
1073
1087
  }
1074
1088
  // console.log(message( null, elem.location, elem, {target,art:assoc}, 'Info','RE')
@@ -1130,7 +1144,8 @@ function resolve( model ) {
1130
1144
  }
1131
1145
  }
1132
1146
  let redirected = null;
1133
- let news = [ { chain: chain.reverse(), sources: [ target ] } ];
1147
+ chain.reverse();
1148
+ let news = [ { chain, sources: [ target ] } ];
1134
1149
  const dict = Object.create(null);
1135
1150
  while (news.length) {
1136
1151
  const outer = news;
@@ -274,6 +274,8 @@ function fns( model ) {
274
274
  function resolveUncheckedPath( ref, expected, user ) {
275
275
  if (!ref.path || ref.path.broken) // incomplete type AST
276
276
  return undefined;
277
+ if (ref._artifact)
278
+ return ref._artifact.name.absolute;
277
279
  const spec = specExpected[expected];
278
280
  let art = (ref.scope === 'global' || spec.global)
279
281
  ? getPathRoot( ref.path, spec, user, {}, model[spec.global || 'definitions'] )
@@ -594,7 +596,8 @@ function fns( model ) {
594
596
  // const { _origin } = user._pathHead;
595
597
  // return (_origin && _origin.kind === '$tableAlias') ? _origin._origin : _origin;
596
598
  }
597
- // if (head.id === 'k') {console.log(Object.keys(user));throw Error(JSON.stringify(user.name))}
599
+ // if (head.id === 'k') {console.log(Object.keys(user));
600
+ // throw new CompilerAssertion(JSON.stringify(user.name))}
598
601
  // if head._artifact is set or is null then it was already computed once
599
602
  if ('_artifact' in head)
600
603
  return Array.isArray(head._artifact) ? false : head._artifact;
@@ -723,8 +726,10 @@ function fns( model ) {
723
726
  } );
724
727
  }
725
728
  else {
729
+ const isVirtual = (user.name?.id === head.id && user.virtual?.val);
730
+ const code = isVirtual ? 'virtual null as ‹name›' : '';
726
731
  signalNotFound( 'ref-undefined-element', [ head.location, user ], valid,
727
- { art: head.id, '#': 'std' } );
732
+ { art: head.id, '#': isVirtual ? 'virtual' : 'std', code } );
728
733
  }
729
734
  }
730
735
  else if (env.$frontend && env.$frontend !== 'cdl' || spec.global) {
@@ -817,7 +822,7 @@ function fns( model ) {
817
822
  // could "change" to this message at the end of compile():
818
823
  message( 'ref-autoexposed', [ item.location, user ], { art },
819
824
  // eslint-disable-next-line max-len
820
- 'An autoexposed entity can\'t be referred to - expose entity $(ART) explicitly' );
825
+ 'An auto-exposed entity can\'t be referred to - expose entity $(ART) explicitly' );
821
826
  }
822
827
  }
823
828
  return art;
@@ -844,8 +849,8 @@ function fns( model ) {
844
849
  }
845
850
 
846
851
  function errorNotFound( item, env ) {
847
- if (!spec.next) { // artifact ref
848
- // TODO: better for TYPE OF, FROM e.Assoc (even disallow for other refs)
852
+ if (!spec.next && artItemsCount >= 0) { // artifact ref
853
+ // TODO: better for FROM e.Assoc (even disallow for other refs)
849
854
  const a = searchName( art, item.id, (spec.envFn || art._subArtifacts) && 'absolute' );
850
855
  signalNotFound( spec.undefinedDef || 'ref-undefined-def', [ item.location, user ],
851
856
  [ env ], { art: a } );
@@ -890,8 +895,10 @@ function fns( model ) {
890
895
  location.$notFound = true;
891
896
  /** @type {object} */
892
897
  const err = message( msgId, location, textParams );
893
- if (valid)
894
- attachAndEmitValidNames(err, ...valid.reverse());
898
+ if (valid) {
899
+ valid.reverse();
900
+ attachAndEmitValidNames(err, ...valid);
901
+ }
895
902
  }
896
903
 
897
904
  /**
@@ -977,10 +984,8 @@ function fns( model ) {
977
984
  // Copy annotations from `ext` to `art`, overwriting inferred ones.
978
985
  // TODO: move to extend.js if not used anymore in define.js
979
986
  function copyAnnotationsForExtensions( ext, art ) {
980
- if (ext.doc)
981
- art.doc = ext.doc; // e.g. through `extensions` array in CSN
982
987
  for (const annoProp in ext) {
983
- if (annoProp.charAt(0) === '@') {
988
+ if (annoProp.charAt(0) === '@' || annoProp === 'doc') {
984
989
  const extAnno = ext[annoProp];
985
990
  if (art[annoProp]?.$inferred)
986
991
  art[annoProp] = extAnno; // overwrite $inferred annos
@@ -141,7 +141,7 @@ function setMemberParent( elem, name, parent, prop ) {
141
141
  else
142
142
  delete elem.name[kind];
143
143
  });
144
- // try { throw new Error('Foo') } catch (e) { elem.name.stack = e; };
144
+ // try { throw new CompilerAssertion('Foo') } catch (e) { elem.name.stack = e; };
145
145
  }
146
146
 
147
147
  /**
@@ -264,27 +264,27 @@ function copyExpr( expr, location, skipUnderscored, rewritePath ) {
264
264
  return r;
265
265
  }
266
266
 
267
- function testExpr( expr, pathTest, queryTest ) {
267
+ function testExpr( expr, pathTest, queryTest, user ) {
268
268
  // TODO: also check path arguments/filters
269
269
  if (!expr || typeof expr === 'string') { // parse error or keywords in {xpr:...}
270
270
  return false;
271
271
  }
272
272
  else if (Array.isArray(expr)) {
273
- return expr.some( e => testExpr( e, pathTest, queryTest ) );
273
+ return expr.some( e => testExpr( e, pathTest, queryTest, user ) );
274
274
  }
275
275
  else if (expr.path) {
276
- return pathTest( expr );
276
+ return pathTest( expr, user );
277
277
  }
278
278
  else if (expr.query) {
279
- return queryTest( expr.query );
279
+ return queryTest( expr.query, user );
280
280
  }
281
281
  else if (expr.op && expr.args) {
282
282
  // unnamed args => array
283
283
  if (Array.isArray(expr.args))
284
- return expr.args.some( e => testExpr( e, pathTest, queryTest ) );
284
+ return expr.args.some( e => testExpr( e, pathTest, queryTest, user ) );
285
285
  // named args => dictionary
286
286
  for (const namedArg of Object.keys(expr.args)) {
287
- if (testExpr(expr.args[namedArg], pathTest, queryTest))
287
+ if (testExpr(expr.args[namedArg], pathTest, queryTest, user))
288
288
  return true;
289
289
  }
290
290
  }