@sap/cds-compiler 3.4.4 → 3.5.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 (129) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +1 -0
  3. package/bin/cds_update_identifiers.js +5 -5
  4. package/bin/cdsc.js +12 -12
  5. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  6. package/doc/CHANGELOG_BETA.md +9 -1
  7. package/doc/CHANGELOG_DEPRECATED.md +2 -0
  8. package/lib/api/main.js +58 -59
  9. package/lib/api/options.js +4 -2
  10. package/lib/api/validate.js +2 -2
  11. package/lib/base/cleanSymbols.js +2 -3
  12. package/lib/base/dictionaries.js +6 -6
  13. package/lib/base/error.js +2 -2
  14. package/lib/base/keywords.js +6 -6
  15. package/lib/base/location.js +11 -12
  16. package/lib/base/message-registry.js +124 -28
  17. package/lib/base/messages.js +247 -179
  18. package/lib/base/model.js +14 -11
  19. package/lib/base/node-helpers.js +9 -10
  20. package/lib/base/optionProcessorHelper.js +138 -129
  21. package/lib/checks/actionsFunctions.js +5 -5
  22. package/lib/checks/annotationsOData.js +4 -4
  23. package/lib/checks/arrayOfs.js +1 -1
  24. package/lib/checks/cdsPersistence.js +1 -1
  25. package/lib/checks/checkForTypes.js +3 -3
  26. package/lib/checks/defaultValues.js +3 -3
  27. package/lib/checks/elements.js +7 -7
  28. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  29. package/lib/checks/foreignKeys.js +1 -1
  30. package/lib/checks/invalidTarget.js +4 -4
  31. package/lib/checks/managedInType.js +1 -1
  32. package/lib/checks/managedWithoutKeys.js +1 -1
  33. package/lib/checks/nonexpandableStructured.js +5 -3
  34. package/lib/checks/nullableKeys.js +1 -1
  35. package/lib/checks/onConditions.js +5 -6
  36. package/lib/checks/parameters.js +1 -1
  37. package/lib/checks/queryNoDbArtifacts.js +2 -2
  38. package/lib/checks/selectItems.js +4 -4
  39. package/lib/checks/sql-snippets.js +4 -4
  40. package/lib/checks/types.js +7 -7
  41. package/lib/checks/utils.js +4 -4
  42. package/lib/checks/validator.js +16 -13
  43. package/lib/compiler/.eslintrc.json +1 -1
  44. package/lib/compiler/assert-consistency.js +0 -1
  45. package/lib/compiler/builtins.js +1 -1
  46. package/lib/compiler/checks.js +73 -15
  47. package/lib/compiler/define.js +3 -7
  48. package/lib/compiler/extend.js +212 -32
  49. package/lib/compiler/finalize-parse-cdl.js +7 -2
  50. package/lib/compiler/index.js +17 -14
  51. package/lib/compiler/populate.js +2 -5
  52. package/lib/compiler/propagator.js +2 -0
  53. package/lib/compiler/shared.js +23 -12
  54. package/lib/compiler/tweak-assocs.js +5 -6
  55. package/lib/compiler/utils.js +6 -0
  56. package/lib/edm/annotations/genericTranslation.js +553 -319
  57. package/lib/edm/annotations/preprocessAnnotations.js +39 -35
  58. package/lib/edm/csn2edm.js +88 -75
  59. package/lib/edm/edm.js +17 -3
  60. package/lib/edm/edmAnnoPreprocessor.js +5 -5
  61. package/lib/edm/edmPreprocessor.js +106 -76
  62. package/lib/edm/edmUtils.js +41 -2
  63. package/lib/gen/Dictionary.json +34 -0
  64. package/lib/gen/language.checksum +1 -1
  65. package/lib/gen/language.interp +66 -63
  66. package/lib/gen/language.tokens +81 -81
  67. package/lib/gen/languageLexer.interp +4 -10
  68. package/lib/gen/languageLexer.js +854 -869
  69. package/lib/gen/languageLexer.tokens +79 -81
  70. package/lib/gen/languageParser.js +14360 -14146
  71. package/lib/inspect/inspectModelStatistics.js +2 -2
  72. package/lib/inspect/inspectPropagation.js +6 -6
  73. package/lib/inspect/inspectUtils.js +2 -2
  74. package/lib/json/from-csn.js +82 -40
  75. package/lib/json/to-csn.js +82 -157
  76. package/lib/language/.eslintrc.json +1 -4
  77. package/lib/language/genericAntlrParser.js +59 -38
  78. package/lib/language/language.g4 +1508 -1490
  79. package/lib/language/multiLineStringParser.js +1 -1
  80. package/lib/main.js +3 -3
  81. package/lib/model/csnUtils.js +130 -122
  82. package/lib/model/revealInternalProperties.js +1 -1
  83. package/lib/model/sortViews.js +4 -6
  84. package/lib/modelCompare/utils/filter.js +4 -3
  85. package/lib/optionProcessor.js +5 -0
  86. package/lib/render/DuplicateChecker.js +1 -1
  87. package/lib/render/manageConstraints.js +12 -12
  88. package/lib/render/toCdl.js +225 -159
  89. package/lib/render/toHdbcds.js +63 -63
  90. package/lib/render/toRename.js +5 -5
  91. package/lib/render/toSql.js +55 -65
  92. package/lib/render/utils/common.js +20 -37
  93. package/lib/render/utils/delta.js +3 -3
  94. package/lib/render/utils/sql.js +22 -6
  95. package/lib/render/utils/stringEscapes.js +3 -3
  96. package/lib/transform/db/applyTransformations.js +3 -3
  97. package/lib/transform/db/assertUnique.js +13 -12
  98. package/lib/transform/db/associations.js +5 -5
  99. package/lib/transform/db/cdsPersistence.js +10 -8
  100. package/lib/transform/db/constraints.js +14 -14
  101. package/lib/transform/db/expansion.js +20 -22
  102. package/lib/transform/db/flattening.js +24 -42
  103. package/lib/transform/db/groupByOrderBy.js +3 -3
  104. package/lib/transform/db/temporal.js +6 -6
  105. package/lib/transform/db/transformExists.js +23 -23
  106. package/lib/transform/db/views.js +16 -16
  107. package/lib/transform/draft/db.js +10 -10
  108. package/lib/transform/draft/odata.js +2 -2
  109. package/lib/transform/forOdataNew.js +12 -40
  110. package/lib/transform/forRelationalDB.js +17 -7
  111. package/lib/transform/localized.js +2 -2
  112. package/lib/transform/odata/toFinalBaseType.js +41 -27
  113. package/lib/transform/odata/typesExposure.js +106 -62
  114. package/lib/transform/parseExpr.js +209 -106
  115. package/lib/transform/transformUtilsNew.js +2 -2
  116. package/lib/transform/translateAssocsToJoins.js +24 -19
  117. package/lib/transform/universalCsn/coreComputed.js +10 -10
  118. package/lib/transform/universalCsn/universalCsnEnricher.js +26 -26
  119. package/lib/transform/universalCsn/utils.js +3 -3
  120. package/lib/utils/file.js +5 -5
  121. package/lib/utils/moduleResolve.js +13 -13
  122. package/lib/utils/objectUtils.js +6 -6
  123. package/lib/utils/term.js +5 -2
  124. package/lib/utils/timetrace.js +51 -24
  125. package/package.json +5 -7
  126. package/share/messages/check-proper-type-of.md +1 -1
  127. package/share/messages/message-explanations.json +1 -1
  128. package/share/messages/redirected-to-complex.md +4 -4
  129. package/share/messages/{syntax-expecting-integer.md → syntax-expecting-unsigned-int.md} +7 -4
@@ -2,6 +2,6 @@
2
2
  "root": true,
3
3
  "extends": "../../.eslintrc-ydkjsi.json",
4
4
  "rules": {
5
- "cds-compiler/space-in-func-decl": "error"
5
+ "cds-compiler/message-no-quotes": "warn"
6
6
  }
7
7
  }
@@ -417,7 +417,6 @@ function assertConsistency( model, stage ) {
417
417
  'args',
418
418
  'func',
419
419
  'suffix',
420
- 'quantifier',
421
420
  '$inferred',
422
421
  '$parens',
423
422
  '_artifact', // _artifact with "localized data"s 'coalesce'
@@ -451,7 +451,7 @@ function initBuiltins( model ) {
451
451
  for (const name in builtins) {
452
452
  const magic = builtins[name];
453
453
  // TODO: rename to $builtinFunction
454
- const art = { kind: 'builtin', name: { element: name, id: name } };
454
+ const art = { kind: 'builtin', name: { id: name, absolute: name } };
455
455
  artifacts[name] = art;
456
456
 
457
457
  if (magic.$autoElement)
@@ -27,7 +27,8 @@ function check( model ) { // = XSN
27
27
  } = model.$messageFunctions;
28
28
  forEachDefinition( model, checkArtifact );
29
29
  forEachGeneric( model, 'vocabularies', checkAnnotationDefinition );
30
- checkSapCommonLocale( model, model.$messageFunctions );
30
+ checkSapCommonLocale( model );
31
+ checkSapCommonTextsAspects( model );
31
32
  return;
32
33
 
33
34
  function checkArtifact( art ) {
@@ -359,13 +360,13 @@ function check( model ) { // = XSN
359
360
  const { literal, val, location } = elem.cardinality[prop];
360
361
  if (!(literal === 'number' && val > 0 ||
361
362
  literal === 'string' && val === '*')) {
362
- error('invalid-cardinality', [ location, elem ], { '#': prop, code: val }, {
363
+ error('invalid-cardinality', [ location, elem ], { '#': prop, code: val, newcode: '*' }, {
363
364
  // eslint-disable-next-line max-len
364
- std: 'Value $(CODE) is invalid for maximum cardinality, expecting a positive number or ‘*’',
365
+ std: 'Value $(CODE) is invalid for maximum cardinality, expecting a positive number or $(NEWCODE)',
365
366
  // eslint-disable-next-line max-len
366
- sourceMax: 'Value $(CODE) is invalid for maximum source cardinality, expecting a positive number or ‘*’',
367
+ sourceMax: 'Value $(CODE) is invalid for maximum source cardinality, expecting a positive number or $(NEWCODE)',
367
368
  // eslint-disable-next-line max-len
368
- targetMax: 'Value $(CODE) is invalid for maximum target cardinality, expecting a positive number or ‘*’',
369
+ targetMax: 'Value $(CODE) is invalid for maximum target cardinality, expecting a positive number or $(NEWCODE)',
369
370
  });
370
371
  }
371
372
  }
@@ -485,7 +486,13 @@ function check( model ) { // = XSN
485
486
  if (Array.isArray(onCond)) // condition in brackets results an array
486
487
  onCond.forEach(Cond => checkAssociationCondition(elem, Cond));
487
488
  else
488
- checkAssociationConditionArgs(elem, onCond.args, onCond.op);
489
+ checkAssociationConditionArgs(elem, onCond.args, getBinaryOp( onCond ));
490
+ }
491
+
492
+ function getBinaryOp( cond ) {
493
+ const { op, args } = cond;
494
+ return op?.val === 'ixpr' && args.length === 3 && args[1].literal === 'token' &&
495
+ args[1] || op;
489
496
  }
490
497
 
491
498
  function checkAssociationConditionArgs( elem, args, op ) {
@@ -495,10 +502,10 @@ function check( model ) { // = XSN
495
502
 
496
503
  function checkAssociationOnCondArg( elem, arg, op ) {
497
504
  if (Array.isArray(arg)) {
498
- arg.forEach(Arg => checkAssociationOnCondArg(elem, Arg, op));
505
+ arg.forEach(Arg => checkAssociationCondition(elem, Arg));
499
506
  }
500
507
  else {
501
- checkAssociationConditionArgs(elem, arg.args, arg.op);
508
+ checkAssociationCondition(elem, arg);
502
509
  singleCheckUnmanagedAssocCondArgumentNoFollowUnmanagedAssoc(elem, arg, op);
503
510
  }
504
511
  }
@@ -537,7 +544,7 @@ function check( model ) { // = XSN
537
544
  }
538
545
 
539
546
  function checkAssociationArgumentStartingWithSelf( op, elem ) {
540
- if (op && op.val === 'xpr') // no check for xpr
547
+ if (op?.val === 'xpr') // no check for xpr, would require re-structuring
541
548
  return;
542
549
  if (op && op.val !== '=')
543
550
  error(null, [ op.location, elem ], {}, '$self comparison is only allowed with \'=\'');
@@ -585,7 +592,7 @@ function check( model ) { // = XSN
585
592
  function checkExpression( xpr, allowAssocTail = false ) {
586
593
  // Since the checks for tree-like and token-stream expressions differ,
587
594
  // check here what kind of expression we are looking at
588
- if (xpr.op && xpr.op.val === 'xpr')
595
+ if (xpr.op?.val === 'xpr')
589
596
  return checkTokenStreamExpression(xpr, allowAssocTail);
590
597
  return checkTreeLikeExpression(xpr, allowAssocTail);
591
598
  }
@@ -921,20 +928,71 @@ function check( model ) { // = XSN
921
928
  }
922
929
  }
923
930
 
931
+ /**
932
+ * Ensure that the sap.common.[Fiori]TextsAspect has proper types for
933
+ * e.g. `locale` and `ID_texts`.
934
+ *
935
+ * @param {XSN.Model} model
936
+ */
937
+ function checkSapCommonTextsAspects( model ) {
938
+ checkSapCommonTextsAspectLocale( model, 'sap.common.TextsAspect' );
939
+ checkSapCommonTextsAspectLocale( model, 'sap.common.FioriTextsAspect' );
940
+
941
+ // Check ID_texts: Fiori requires it to be UUID.
942
+ const fioriTextsAspect = model.definitions['sap.common.FioriTextsAspect'];
943
+ const id = fioriTextsAspect?.elements?.ID_texts;
944
+ if (id) {
945
+ const idType = id._effectiveType;
946
+ if (!idType || idType.name?.absolute !== 'cds.UUID') {
947
+ const { error } = model.$messageFunctions;
948
+ error('def-invalid-element-type', [ id.type.location, id ], {
949
+ '#': 'std',
950
+ art: 'sap.common.FioriTextsAspect',
951
+ elemref: 'ID_texts',
952
+ type: 'cds.UUID',
953
+ });
954
+ }
955
+ }
956
+ }
957
+
958
+ /**
959
+ * Ensure that the `locale` element of sap.common.[Fiori]TextsAspects
960
+ * is a string type. This is required by CAP runtimes to work properly.
961
+ *
962
+ * @param {XSN.Model} model
963
+ * @param {string} name Either sap.common.TextsAspects or sap.common.FioriTextsAspects
964
+ */
965
+ function checkSapCommonTextsAspectLocale( model, name ) {
966
+ const locale = model.definitions[name]?.elements?.locale;
967
+ if (locale) {
968
+ // `locale` could also be `sap.common.Locale`, which must also be a string.
969
+ const type = locale._effectiveType;
970
+ if (type?.name?.absolute !== 'cds.String') {
971
+ const hasCommonLocale = !!model.definitions['sap.common.Locale'];
972
+ const { error } = model.$messageFunctions;
973
+ error('def-invalid-element-type', [ locale.type.location, locale ], {
974
+ '#': hasCommonLocale ? 'texts-aspect-locale' : 'std',
975
+ art: name,
976
+ elemref: 'locale',
977
+ type: 'cds.String',
978
+ othertype: 'sap.common.Locale',
979
+ });
980
+ }
981
+ }
982
+ }
983
+
924
984
  /**
925
985
  * Checks that sap.common.Locale is of type cds.String. This limitation may
926
986
  * be lifted later on.
927
987
  *
928
988
  * @param {XSN.Model} model
929
- * @param {object} messageFunctions
930
989
  */
931
- function checkSapCommonLocale( model, messageFunctions ) {
990
+ function checkSapCommonLocale( model ) {
932
991
  const localeArt = model.definitions['sap.common.Locale'];
933
992
  if (localeArt) {
934
993
  const type = localeArt._effectiveType;
935
- const isCdsString = type && type.name && type.name.absolute === 'cds.String';
936
- if (!isCdsString) {
937
- const { message } = messageFunctions;
994
+ if (type?.name?.absolute !== 'cds.String') {
995
+ const { message } = model.$messageFunctions;
938
996
  message('type-expected-builtin', [ localeArt.name.location, localeArt ],
939
997
  { name: 'sap.common.Locale' },
940
998
  'Expected $(NAME) to be a string type');
@@ -132,6 +132,7 @@ const {
132
132
  pathName,
133
133
  splitIntoPath,
134
134
  annotationHasEllipsis,
135
+ isDirectComposition,
135
136
  } = require('./utils');
136
137
  const { compareLayer } = require('./moduleLayers');
137
138
  const { initBuiltins, isInReservedNamespace } = require('./builtins');
@@ -837,7 +838,7 @@ function define( model ) {
837
838
  dictAddArray( p.$tableAliases, table.name.id, table );
838
839
  }
839
840
  if (table.name.id[0] === '$') {
840
- warning( 'syntax-dollar-ident', [ table.name.location, table ], {
841
+ warning( 'name-invalid-dollar-alias', [ table.name.location, table ], {
841
842
  '#': (table.name.$inferred ? '$tableImplicit' : '$tableAlias'),
842
843
  name: '$',
843
844
  keyword: 'as',
@@ -924,7 +925,7 @@ function define( model ) {
924
925
  error( 'duplicate-definition', [ loc, query ], { name: dupName, '#': 'alias' } );
925
926
  } );
926
927
  if (mixin.name.id[0] === '$') {
927
- warning( 'syntax-dollar-ident', [ mixin.name.location, mixin ],
928
+ warning( 'name-invalid-dollar-alias', [ mixin.name.location, mixin ],
928
929
  { '#': 'mixin', name: '$' } );
929
930
  }
930
931
  }
@@ -932,11 +933,6 @@ function define( model ) {
932
933
  }
933
934
  }
934
935
 
935
- function isDirectComposition( art ) {
936
- const type = art.type && art.type.path;
937
- return type && type[0] && type[0].id === 'cds.Composition';
938
- }
939
-
940
936
  // Return whether the `target` is actually a `targetAspect`
941
937
  // TODO: really do that here and not in kick-start.js?
942
938
  function targetIsTargetAspect( elem ) {
@@ -20,7 +20,7 @@ const {
20
20
  setMemberParent,
21
21
  dependsOnSilent,
22
22
  augmentPath,
23
- splitIntoPath,
23
+ splitIntoPath, isDirectComposition,
24
24
  } = require('./utils');
25
25
  const layers = require('./moduleLayers');
26
26
  const { typeParameters } = require('./builtins');
@@ -54,6 +54,7 @@ function extend( model ) {
54
54
  applyExtensions();
55
55
 
56
56
  const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
57
+ const useTextsAspects = checkTextsAspects();
57
58
 
58
59
  Object.keys( model.definitions ).forEach( processArtifact );
59
60
 
@@ -293,6 +294,97 @@ function extend( model ) {
293
294
  });
294
295
  }
295
296
 
297
+ /**
298
+ * Check that special `sap.common.*` aspects for `.texts` entities are
299
+ * consistent with compiler expectations. Emits messages and returns
300
+ * false if the aspects are not valid.
301
+ *
302
+ * @return {boolean}
303
+ */
304
+ function checkTextsAspects() {
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
+ }
321
+
322
+ return !hasError;
323
+ }
324
+
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 });
328
+ return false;
329
+ }
330
+
331
+ let hasError = false;
332
+ if (addTextsLanguageAssoc && art.elements.language) {
333
+ const lang = art.elements.language;
334
+ error('def-unexpected-element', [ lang.name.location, lang ],
335
+ { option: 'addTextsLanguageAssoc', art, name: 'language' },
336
+ // eslint-disable-next-line max-len
337
+ '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
338
+ hasError = true;
339
+ }
340
+
341
+ for (const name in specialElements) {
342
+ const expected = specialElements[name];
343
+ const elem = art.elements[name];
344
+ if (!elem) {
345
+ error('def-invalid-texts-aspect', [ art.name.location, art ],
346
+ { '#': 'missing', art, name });
347
+ hasError = true;
348
+ }
349
+ else if (expected.key !== undefined && !!elem.key?.val !== expected.key) {
350
+ const loc = elem.key?.location || elem.name?.location || art.name.location;
351
+ error('def-invalid-texts-aspect', [ loc, elem ],
352
+ { '#': expected.key ? 'key' : 'no-key', art: elem });
353
+ hasError = true;
354
+ }
355
+ }
356
+
357
+ if (hasError) // avoid subsequent errors, if the special elements are already wrong
358
+ return false;
359
+
360
+ for (const name in art.elements) {
361
+ const elem = art.elements[name];
362
+ const include = elem.$inferred === 'include';
363
+ if (!specialElements[name] && elem.key) {
364
+ const loc = include ? elem.location : elem.key.location;
365
+ error( 'def-unexpected-key', [ loc, elem ],
366
+ { '#': !include ? 'std' : 'include', art } );
367
+ hasError = true;
368
+ }
369
+ else if (hasTruthyProp( elem, 'localized' )) {
370
+ // TODO: T:loc, i.e. "localized" from other type (needs resolver?)
371
+ // Not supported anyway, but important for recompilation (which fails correctly).
372
+ const loc = elem.localized?.location || elem.location;
373
+ error( 'def-unexpected-localized', [ loc, elem ],
374
+ { '#': !include ? 'std' : 'include', art } );
375
+ hasError = true;
376
+ }
377
+ else if (elem.targetAspect) {
378
+ error( 'def-unexpected-composition', [ elem.targetAspect.location, elem ], { art },
379
+ '$(ART) can\'t have composition of aspects' );
380
+ hasError = true;
381
+ }
382
+ }
383
+
384
+ return !hasError;
385
+ }
386
+
387
+
296
388
  /**
297
389
  * Copy columns for EXTEND PROJECTION
298
390
  *
@@ -614,7 +706,7 @@ function extend( model ) {
614
706
  function applyIncludes( ext, art ) {
615
707
  if (kindProperties[art.kind].include !== true) {
616
708
  error('extend-unexpected-include', [ ext.includes[0]?.location, ext ],
617
- { kind: art.kind });
709
+ { meta: art.kind });
618
710
  return;
619
711
  }
620
712
 
@@ -696,9 +788,8 @@ function extend( model ) {
696
788
  return;
697
789
  if (textsEntity) // expanded localized data in source
698
790
  return; // -> make it idempotent
699
- const newTextsEntity = createTextsEntity( art, textsName, localized, fioriEnabled );
791
+ createTextsEntity( art, textsName, localized, fioriEnabled );
700
792
  addTextsAssociations( art, textsName, localized );
701
- copyPersistenceAnnotations(newTextsEntity, art, options);
702
793
  }
703
794
 
704
795
  /**
@@ -714,6 +805,9 @@ function extend( model ) {
714
805
  let keys = 0;
715
806
  const textElems = [];
716
807
  const conflictingElements = [];
808
+ // These elements are required or the localized-mechanism does not work.
809
+ // Other elements from sap.common.TextsAspect may be "overridden" as per
810
+ // usual include-mechanism.
717
811
  const protectedElements = [ 'locale', 'texts', 'localized' ];
718
812
  if (fioriEnabled)
719
813
  protectedElements.push('ID_texts');
@@ -798,16 +892,37 @@ function extend( model ) {
798
892
  * @param {boolean} fioriEnabled
799
893
  */
800
894
  function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
801
- const art = createTextsEntityWithDefaultElements( 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
899
+ ? createTextsEntityWithInclude( base, absolute, fioriEnabled )
900
+ : createTextsEntityWithDefaultElements( base, absolute, fioriEnabled );
802
901
 
803
902
  const { location } = base.name;
804
- const { locale } = art.elements;
903
+
904
+ if (addTextsLanguageAssoc) {
905
+ const language = {
906
+ name: { location, id: 'language' },
907
+ kind: 'element',
908
+ location,
909
+ type: augmentPath( location, 'cds.Association' ),
910
+ target: augmentPath( location, 'sap.common.Languages' ),
911
+ on: {
912
+ op: { val: '=', location },
913
+ args: [
914
+ { path: [ { id: 'language', location }, { id: 'code', location } ], location },
915
+ { path: [ { id: 'locale', location } ], location },
916
+ ],
917
+ location,
918
+ },
919
+ };
920
+ setLink( language, '_block', model.$internal );
921
+ dictAdd( art.elements, 'language', language );
922
+ }
805
923
 
806
924
  // assertUnique array value, first entry is 'locale'
807
- const assertUniqueValue = [ {
808
- path: [ { id: locale.name.id, location: locale.location } ],
809
- location: locale.location,
810
- } ];
925
+ const assertUniqueValue = [];
811
926
 
812
927
  for (const orig of textElems) {
813
928
  const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
@@ -833,13 +948,73 @@ function extend( model ) {
833
948
  elem.localized = { val: null, $inferred: 'localized', location: localized.location };
834
949
  }
835
950
  }
836
- if (fioriEnabled)
951
+
952
+ initArtifact( art );
953
+ if (art.includes) {
954
+ // add elements `locale`, etc. which are required below.
955
+ applyIncludes(art, art);
956
+ }
957
+
958
+ if (fioriEnabled ) {
959
+ const { locale } = art.elements;
960
+ assertUniqueValue.unshift({
961
+ path: [ { id: locale.name.id, location: locale.location } ],
962
+ location: locale.location,
963
+ });
837
964
  annotateWith( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' );
965
+ }
966
+
967
+ copyPersistenceAnnotations(art, base, options);
968
+ return art;
969
+ }
838
970
 
971
+ /**
972
+ * Create the `.texts` entity for the given base artifact.
973
+ * In contrast to createTextsEntityWithDefaultElements(), this one creates
974
+ * an include for `sap.common.TextsAspect`.
975
+ *
976
+ * Does NOT apply the include!
977
+ *
978
+ * TODO: When beta flag textsAspect is removed, update caller-site and remove old coding.
979
+ *
980
+ * @param {XSN.Artifact} base
981
+ * @param {string} absolute
982
+ * @param {boolean} fioriEnabled
983
+ */
984
+ function createTextsEntityWithInclude( base, absolute, fioriEnabled ) {
985
+ const elements = Object.create(null);
986
+ const { location } = base.name;
987
+
988
+ const include = fioriEnabled ? 'sap.common.FioriTextsAspect' : 'sap.common.TextsAspect';
989
+ const art = {
990
+ kind: 'entity',
991
+ name: { path: splitIntoPath( location, absolute ), absolute, location },
992
+ includes: [ createInclude( include, base.location ) ],
993
+ location: base.location,
994
+ elements,
995
+ $inferred: 'localized-entity',
996
+ };
997
+
998
+ if (!fioriEnabled) {
999
+ // To be compatible, we switch off draft without @fiori.draft.enabled
1000
+ // TODO (next major version): remove?
1001
+ annotateWith( art, '@odata.draft.enabled', art.location, false );
1002
+ }
1003
+
1004
+ if (addTextsLanguageAssoc && art.elements.language)
1005
+ art.elements.language = undefined; // TODO: Message? Ignore?
1006
+
1007
+ setLink( art, '_block', model.$internal );
1008
+ model.definitions[absolute] = art;
839
1009
  return art;
840
1010
  }
841
1011
 
842
- function createTextsEntityWithDefaultElements( base, absolute, textElems, fioriEnabled ) {
1012
+ /**
1013
+ * @param {XSN.Artifact} base
1014
+ * @param {string} absolute
1015
+ * @param {boolean} fioriEnabled
1016
+ */
1017
+ function createTextsEntityWithDefaultElements( base, absolute, fioriEnabled ) {
843
1018
  const elements = Object.create(null);
844
1019
  const { location } = base.name;
845
1020
  const art = {
@@ -879,28 +1054,8 @@ function extend( model ) {
879
1054
  }
880
1055
 
881
1056
  dictAdd( art.elements, 'locale', locale );
882
- if (addTextsLanguageAssoc) {
883
- const language = {
884
- name: { location, id: 'language' },
885
- kind: 'element',
886
- location,
887
- type: augmentPath( location, 'cds.Association' ),
888
- target: augmentPath( location, 'sap.common.Languages' ),
889
- on: {
890
- op: { val: '=', location },
891
- args: [
892
- { path: [ { id: 'language', location }, { id: 'code', location } ], location },
893
- { path: [ { id: 'locale', location } ], location },
894
- ],
895
- location,
896
- },
897
- };
898
- setLink( language, '_block', model.$internal );
899
- dictAdd( art.elements, 'language', language );
900
- }
901
1057
  setLink( art, '_block', model.$internal );
902
1058
  model.definitions[absolute] = art;
903
- initArtifact( art );
904
1059
 
905
1060
  return art;
906
1061
  }
@@ -943,6 +1098,22 @@ function extend( model ) {
943
1098
  setLink( localized, '_block', model.$internal );
944
1099
  }
945
1100
 
1101
+ /**
1102
+ * Create a structure that can be used as an item in `includes`.
1103
+ *
1104
+ * @param {string} name
1105
+ * @param {XSN.Location} location
1106
+ */
1107
+ function createInclude( name, location ) {
1108
+ const include = {
1109
+ path: [ { id: name, location } ],
1110
+ location,
1111
+ };
1112
+ setArtifactLink( include.path[0], model.definitions[name] );
1113
+ setArtifactLink( include, model.definitions[name] );
1114
+ return include;
1115
+ }
1116
+
946
1117
  /**
947
1118
  * Returns whether `art` directly or indirectly has the property 'prop',
948
1119
  * following the 'origin' and the 'type' (not involving elements).
@@ -1092,6 +1263,15 @@ function extend( model ) {
1092
1263
  });
1093
1264
  return false;
1094
1265
  }
1266
+
1267
+ if (elem.type && !isDirectComposition(elem)) {
1268
+ // Only issue warning for direct usages, not for projections, includes, etc.
1269
+ // TODO: Make it configurable error; v4: error
1270
+ warning( 'def-expected-comp-aspect', [ elem.type.location, elem ],
1271
+ { prop: 'Composition of', otherprop: 'Association to' },
1272
+ 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect');
1273
+ }
1274
+
1095
1275
  return true;
1096
1276
  }
1097
1277
 
@@ -186,8 +186,13 @@ function finalizeParseCdl( model ) {
186
186
  // For better error messages, check for invalid TYPE OFs similarly
187
187
  // to how `resolveType()` does.
188
188
  let struct = artWithType;
189
- while (struct.kind === 'element')
190
- struct = struct._parent;
189
+ // `items` have no kind, but need to be skipped as well
190
+ while (struct.kind === 'element' || struct._outer?.items) {
191
+ if (struct._outer?.items)
192
+ struct = struct._outer;
193
+ else
194
+ struct = struct._parent;
195
+ }
191
196
  if (struct.kind === 'select' || struct.kind === 'annotation' || struct !== user._main) {
192
197
  message( 'type-unexpected-typeof', [ artWithType.type.location, user ],
193
198
  { keyword: 'type of', '#': struct.kind } );
@@ -40,8 +40,14 @@ const { cdsFs } = require('../utils/file');
40
40
  const fs = require('fs');
41
41
  const path = require('path');
42
42
 
43
- const csnExtensions = [ '.json', '.csn' ];
44
- const cdlExtensions = [ '.cds', '.hdbcds', '.hdbdd', '.cdl' ];
43
+ const extensionParsers = {
44
+ csn: parseCsn.parse,
45
+ json: parseCsn.parse,
46
+ cds: parseLanguage,
47
+ cdl: parseLanguage,
48
+ hdbcds: parseLanguage,
49
+ hdbdd: parseLanguage,
50
+ };
45
51
 
46
52
  // Class for command invocation errors. Additional members:
47
53
  // `errors`: vector of errors (file IO or ArgumentError)
@@ -75,21 +81,18 @@ class ArgumentError extends Error {
75
81
  function parseX( source, filename, options = {}, messageFunctions = null ) {
76
82
  if (!messageFunctions)
77
83
  messageFunctions = createMessageFunctions( options, 'parse' );
78
- const ext = path.extname( filename ).toLowerCase();
79
- if (csnExtensions.includes(ext) || options.fallbackParser === 'csn!')
80
- return parseCsn.parse( source, filename, options, messageFunctions );
81
- if (cdlExtensions.includes(ext))
82
- return parseLanguage( source, filename, options, messageFunctions );
83
- if (options.fallbackParser === 'csn')
84
- return parseCsn.parse( source, filename, options, messageFunctions );
85
- if (options.fallbackParser) // any other value: like 'cdl' (historic reasons)
86
- return parseLanguage( source, filename, options, messageFunctions );
87
- if (source.startsWith('{')) // Source may be JSON.
88
- return parseCsn.parse( source, filename, options, messageFunctions );
84
+ const ext = path.extname( filename ).slice(1).toLowerCase();
85
+ // eslint-disable-next-line no-nested-ternary
86
+ const parser = options.fallbackParser === 'auto!'
87
+ ? (source?.startsWith('{') ? parseCsn.parse : parseLanguage)
88
+ : (extensionParsers[ext] || extensionParsers[options.fallbackParser] ||
89
+ source.startsWith('{') && parseCsn.parse);
90
+ if (parser)
91
+ return parser( source, filename, options, messageFunctions );
89
92
 
90
93
  const model = { location: { file: filename } };
91
94
  messageFunctions.error( 'file-unknown-ext', emptyWeakLocation(filename),
92
- { file: ext && ext.slice(1), '#': !ext && 'none' }, {
95
+ { file: ext, '#': !ext && 'none' }, {
93
96
  std: 'Unknown file extension $(FILE)',
94
97
  none: 'No file extension',
95
98
  } );
@@ -198,12 +198,9 @@ function populate( model ) {
198
198
  if (a.expand && expandFromColumns( a, art, cardinality ) ||
199
199
  art.target && redirectImplicitly( a, art ) ||
200
200
  art.elements && expandElements( a, art ) ||
201
- art.items && expandItems( a, art ))
201
+ art.items && expandItems( a, art ) ||
202
+ art.enum && expandEnum( a, art ))
202
203
  art = a;
203
-
204
- else if (art.enum && expandEnum( a, art ))
205
- art = a;
206
-
207
204
  setLink( a, '_effectiveType', art );
208
205
  }
209
206
  }
@@ -15,6 +15,7 @@ const {
15
15
  isDeprecatedEnabled,
16
16
  } = require( '../base/model');
17
17
  const { setLink, linkToOrigin, withAssociation } = require('./utils');
18
+ const $inferred = Symbol.for('cds.$inferred');
18
19
  // const { refString } = require( '../base/messages')
19
20
 
20
21
  // Note that propagation here is also used for deep-copying (function `onlyViaParent`)
@@ -225,6 +226,7 @@ function propagate( model ) {
225
226
  member.$inferred = 'proxy';
226
227
  setEffectiveType(member, dict[name]);
227
228
  }
229
+ target[prop][$inferred] = 'prop';
228
230
  }
229
231
 
230
232
  function onlyViaParent( prop, target, source ) {