@sap/cds-compiler 3.8.2 → 3.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bin/cdsc.js +2 -2
  3. package/doc/CHANGELOG_BETA.md +26 -5
  4. package/lib/api/.eslintrc.json +3 -2
  5. package/lib/api/options.js +3 -1
  6. package/lib/api/validate.js +1 -1
  7. package/lib/base/message-registry.js +28 -19
  8. package/lib/base/messages.js +6 -1
  9. package/lib/base/model.js +2 -2
  10. package/lib/checks/.eslintrc.json +1 -0
  11. package/lib/checks/actionsFunctions.js +6 -6
  12. package/lib/checks/annotationsOData.js +1 -1
  13. package/lib/checks/elements.js +28 -17
  14. package/lib/checks/foreignKeys.js +1 -1
  15. package/lib/checks/invalidTarget.js +1 -1
  16. package/lib/checks/onConditions.js +11 -6
  17. package/lib/checks/queryNoDbArtifacts.js +1 -1
  18. package/lib/checks/types.js +1 -1
  19. package/lib/checks/utils.js +1 -1
  20. package/lib/checks/validator.js +3 -2
  21. package/lib/compiler/assert-consistency.js +7 -2
  22. package/lib/compiler/base.js +8 -4
  23. package/lib/compiler/builtins.js +7 -0
  24. package/lib/compiler/checks.js +73 -6
  25. package/lib/compiler/define.js +10 -5
  26. package/lib/compiler/extend.js +910 -1711
  27. package/lib/compiler/finalize-parse-cdl.js +1 -1
  28. package/lib/compiler/generate.js +838 -0
  29. package/lib/compiler/index.js +2 -0
  30. package/lib/compiler/populate.js +2 -2
  31. package/lib/compiler/propagator.js +20 -8
  32. package/lib/compiler/resolve.js +3 -3
  33. package/lib/compiler/shared.js +3 -1
  34. package/lib/edm/annotations/genericTranslation.js +18 -8
  35. package/lib/edm/csn2edm.js +14 -14
  36. package/lib/edm/edm.js +25 -11
  37. package/lib/edm/edmPreprocessor.js +47 -23
  38. package/lib/edm/edmUtils.js +37 -9
  39. package/lib/gen/Dictionary.json +5 -7
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +3 -1
  42. package/lib/gen/language.tokens +24 -23
  43. package/lib/gen/languageLexer.interp +4 -1
  44. package/lib/gen/languageLexer.js +792 -784
  45. package/lib/gen/languageLexer.tokens +12 -11
  46. package/lib/gen/languageParser.js +3564 -3493
  47. package/lib/json/from-csn.js +28 -6
  48. package/lib/json/to-csn.js +10 -6
  49. package/lib/language/antlrParser.js +11 -3
  50. package/lib/language/genericAntlrParser.js +2 -1
  51. package/lib/language/language.g4 +14 -3
  52. package/lib/model/csnRefs.js +10 -5
  53. package/lib/model/csnUtils.js +41 -76
  54. package/lib/modelCompare/utils/.eslintrc.json +1 -1
  55. package/lib/optionProcessor.js +7 -4
  56. package/lib/render/.eslintrc.json +1 -1
  57. package/lib/render/toCdl.js +244 -168
  58. package/lib/render/toHdbcds.js +18 -10
  59. package/lib/render/toSql.js +24 -2
  60. package/lib/transform/db/.eslintrc.json +4 -3
  61. package/lib/transform/db/cdsPersistence.js +1 -1
  62. package/lib/transform/db/expansion.js +11 -6
  63. package/lib/transform/db/flattening.js +22 -15
  64. package/lib/transform/db/rewriteCalculatedElements.js +50 -29
  65. package/lib/transform/db/temporal.js +1 -1
  66. package/lib/transform/db/views.js +1 -1
  67. package/lib/transform/draft/db.js +1 -1
  68. package/lib/transform/draft/odata.js +3 -4
  69. package/lib/transform/forOdataNew.js +5 -6
  70. package/lib/transform/forRelationalDB.js +7 -7
  71. package/lib/transform/localized.js +1 -1
  72. package/lib/transform/odata/toFinalBaseType.js +6 -6
  73. package/lib/transform/odata/typesExposure.js +12 -3
  74. package/lib/transform/odata/utils.js +3 -0
  75. package/lib/transform/transformUtilsNew.js +11 -26
  76. package/lib/transform/translateAssocsToJoins.js +9 -9
  77. package/lib/transform/universalCsn/.eslintrc.json +3 -2
  78. package/lib/transform/universalCsn/coreComputed.js +1 -1
  79. package/lib/transform/universalCsn/universalCsnEnricher.js +6 -4
  80. package/lib/utils/file.js +3 -3
  81. package/lib/utils/moduleResolve.js +1 -1
  82. package/package.json +1 -1
@@ -1,14 +1,10 @@
1
- // Extend, include, localized data and managed compositions
2
-
3
- // Is handled together in this file because people want to extend the generated
4
- // definitions in the future.
1
+ // Extend
5
2
 
6
3
  'use strict';
7
4
 
8
5
  const { searchName, weakLocation } = require('../base/messages');
9
6
  const {
10
- isDeprecatedEnabled,
11
- forEachGeneric, forEachInOrder, forEachDefinition,
7
+ forEachInOrder, forEachDefinition,
12
8
  forEachMember,
13
9
  isBetaEnabled,
14
10
  } = require('../base/model');
@@ -18,15 +14,10 @@ const {
18
14
  setLink,
19
15
  setArtifactLink,
20
16
  copyExpr,
21
- setAnnotation,
22
17
  setExpandStatusAnnotate,
23
18
  linkToOrigin,
24
- setMemberParent,
25
19
  dependsOnSilent,
26
- augmentPath,
27
20
  pathName,
28
- splitIntoPath,
29
- isDirectComposition,
30
21
  annotationHasEllipsis,
31
22
  } = require('./utils');
32
23
  const layers = require('./moduleLayers');
@@ -35,6 +26,8 @@ const $location = Symbol.for('cds.$location');
35
26
 
36
27
  const genLocation = { file: '' }; // attach stupid location - TODO: remove in v4
37
28
 
29
+ // Array.prototype.spread = 42; // prototype-polluted JS classes
30
+
38
31
  function extend( model ) {
39
32
  const { options } = model;
40
33
  // Get simplified "resolve" functionality and the message function:
@@ -46,84 +39,24 @@ function extend( model ) {
46
39
  resolveUncheckedPath,
47
40
  resolveTypeArgumentsUnchecked,
48
41
  attachAndEmitValidNames,
49
- initArtifact,
50
42
  initMembers,
51
43
  } = model.$functions;
52
44
 
53
45
  Object.assign( model.$functions, {
54
- lateExtensions,
55
- chooseAnnotationsInArtifact,
46
+ createRemainingAnnotateStatements,
47
+ extendArtifactBefore,
56
48
  extendArtifactAfter,
49
+ applyIncludes, // TODO: re-check
57
50
  } );
58
51
 
59
- const extensionsDict = Object.create(null);
52
+ sortModelSources();
53
+ const extensionsDict = Object.create(null); // TODO TMP
60
54
  forEachDefinition( model, tagIncludes ); // TODO TMP
61
55
 
62
- forEachDefinition( model, chooseAnnotationsInArtifact );
63
- applyExtensions();
64
-
65
- const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
66
- const useTextsAspect = checkTextsAspect();
67
-
68
- Object.keys( model.definitions ).forEach( processArtifact );
69
-
70
- compositionChildPersistence();
56
+ forEachDefinition( model, extendArtifactBefore );
57
+ applyExtensions(); // old-style
71
58
  return;
72
59
 
73
- /**
74
- * Process "composition of" artifacts.
75
- *
76
- * @param {string} name
77
- */
78
- function processArtifact( name ) {
79
- const art = model.definitions[name];
80
- if (!(art.$duplicates)) {
81
- processAspectComposition( art );
82
- if (art.kind === 'entity' && !art.query && art.elements)
83
- // check potential entity parse error
84
- processLocalizedData( art );
85
- }
86
- }
87
-
88
- /**
89
- * Propagate the given `prop` (e.g. annotation) early, i.e. copy it from all `.includes`
90
- * if they have the property.
91
- *
92
- * @param {XSN.Definition} art
93
- * @param {string} prop
94
- */
95
- function propagateEarly( art, prop ) {
96
- if (art[prop])
97
- return;
98
- for (const ref of art.includes) {
99
- const aspect = ref._artifact;
100
- if (aspect) {
101
- const anno = aspect[prop];
102
- if (anno && (anno.val !== null || !art[prop]))
103
- art[prop] = Object.assign( { $inferred: 'include' }, anno );
104
- }
105
- }
106
- }
107
-
108
- /**
109
- * Copy `@cds.persistence.skip` and `@cds.persistence.skip` from parent to child
110
- * for managed compositions. This needs to be done after extensions, i.e. annotations,
111
- * have been applied or `annotate E.comp` would not have an effect on `E.comp.subComp`.
112
- */
113
- function compositionChildPersistence() {
114
- const processed = new WeakSet();
115
- forEachDefinition(model, processCompositionPersistence);
116
-
117
- function processCompositionPersistence( def ) {
118
- if (def.$inferred === 'composition-entity' && !processed.has(def)) {
119
- if (def._parent)
120
- processCompositionPersistence(def._parent);
121
- copyPersistenceAnnotations( def, def._parent );
122
- processed.add(def);
123
- }
124
- }
125
- }
126
-
127
60
  // TMP:
128
61
  function tagIncludes( art ) {
129
62
  if (art.includes)
@@ -135,6 +68,43 @@ function extend( model ) {
135
68
  //-----------------------------------------------------------------------------
136
69
  // extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements
137
70
 
71
+ /**
72
+ * Goes through all (applied) annotations in the given artifact and chooses one
73
+ * if multiple exist according to the module layer.
74
+ * TODO: update comment if extension algorithm is finished
75
+ *
76
+ * @param {XSN.Artifact} art
77
+ */
78
+ function extendArtifactBefore( art ) {
79
+ // for main artifacts, move extensions from `$collectedExtensions` model dictionary:
80
+ if (!art._main && !art._outer && art._extensions === undefined &&
81
+ art.kind !== 'namespace') {
82
+ // if (!art.name) console.log(art)
83
+ const { absolute } = art.name;
84
+ setLink( art, '_extensions', model.$collectedExtensions[absolute]?._extensions || null );
85
+ if (art._extensions && !art.builtin) { // keep extensions for builtin in $collectedExtensions
86
+ delete model.$collectedExtensions[absolute];
87
+ // TODO: if the extension mechanism has been completed, we could uncomment:
88
+ // art._extensions.forEach( ext => resolvePath( ext.name, ext.kind, ext )); // for LSP
89
+ // for now, we do that at the end of createRemainingAnnotateStatements()
90
+ }
91
+ }
92
+ if (art._extensions) {
93
+ // TODO: the following function can now be simplified
94
+ // if (art.$inferred) console.log('CAI:', art.name, art.$inferred,art._extensions)
95
+ // With extensions, member appears in CSN, affects directly the rendering of
96
+ // elements etc. TODO: do that more specifically on the dicts (via symbol)
97
+ // Probably better: we could use the _extensions dict prop directly in to-csn
98
+ if (art.$inferred)
99
+ setExpandStatusAnnotate( art, 'annotate' );
100
+ if (Array.isArray( art._extensions )) {
101
+ checkExtensionsKind( art._extensions, art ); // TODO: check with builtins
102
+ transformArtifactExtensions( art );
103
+ }
104
+ applyAllExtensions( art );
105
+ }
106
+ }
107
+
138
108
  // TODO: assert that we have not yet transformed/used _extensions on sub elements
139
109
  // TODO necessary(?): transformArtifactExtensions must ensure that each annotate
140
110
  // is in either returns,items,elements,enum
@@ -184,12 +154,12 @@ function extend( model ) {
184
154
  /**
185
155
  * Create super annotate statements for remaining extensions
186
156
  */
187
- function lateExtensions() { // -> createRemainingAnnotateStatements
188
- model.extensions = Object.values( model.$lateExtensions );
157
+ function createRemainingAnnotateStatements() {
158
+ model.extensions = Object.values( model.$collectedExtensions );
189
159
  // TODO: testMode sort?
190
160
  model.extensions.forEach( createSuperAnnotate );
191
161
  // set _artifact links for “main extensions” late as it would disturb the
192
- // still existing old extend mechanism, see chooseAnnotationsInArtifact(),
162
+ // still existing old extend mechanism, see extendArtifactBefore(),
193
163
  // needed for LSP and friends:
194
164
  Object.values( model.sources ).forEach( setArtifactLinkForExtensions );
195
165
  Object.values( model.definitions ).forEach( setArtifactLinkForExtensions );
@@ -206,1704 +176,971 @@ function extend( model ) {
206
176
  }
207
177
  }
208
178
 
209
- // For extendArtifactAfter(): -------------------------------------------------
179
+ // For extendArtifactBefore(): ------------------------------------------------
210
180
 
211
- // Remarks on messages: we allow the type extensions only if the artifact
212
- // originally had that property → any check of the kind “type prop can only be
213
- // used with FooBar” is independent from `extend … with type`. Function
214
- // checkTypeArguments() in resolve.js reports 'type-unexpected-argument', but
215
- // that is currently incomplete.
216
- //
217
- // We then report (in the future), use the first message of:
218
- // - the usual messages if a type argument is wrong, independently from `extend`
219
- // - 'ext-unexpected-type-argument' (TODO) if the artifact does not have the prop
220
- // - 'ext-invalid-type-argument' if the value is wrong for extend (no overwrite)
221
- //
222
- // TODO v4: do not allow `extend … with (precision: …)` alone if original def also has `scale`
223
- function applyTypeExtensions( art, ext, prop, scaleDiff ) {
224
- // console.log('ATE:',art?.[prop],ext?.[prop],scaleDiff)
225
- if (!ext?.[prop])
226
- return 0;
227
- if (!art[prop]) {
228
- const isBuiltin = art._effectiveType?.builtin;
229
- if (isBuiltin && !allowsTypeArgument( art, prop )) {
230
- // Let checkTypeArguments() in resolve.js report a message, is incomplete
231
- // though, i.e. can only safely be used for scalars at the moment. But we
232
- // will improve that function and not try to do extra things here.
233
- art[prop] = ext[prop]; // enable checkTypeArguments() doing its job
234
- return 0;
181
+ function checkExtensionsKind( extensions, art ) {
182
+ for (const ext of extensions) {
183
+ const kind = ext.expectedKind?.val;
184
+ if (kind && kind !== art.kind) {
185
+ const loc = ext.expectedKind.location;
186
+ if (kind === 'context' || kind === 'service') {
187
+ // We have no real artifact during the construction of a super-annotate statement:
188
+ const msgArgs = {
189
+ '#': (art.kind === 'service' || art.kind === 'annotate') ? art.kind : 'std',
190
+ art,
191
+ kind,
192
+ code: 'extend … with definitions',
193
+ keyword: 'extend service',
194
+ };
195
+ warning( 'ext-invalid-kind', [ loc, ext ], msgArgs, {
196
+ std: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) instead',
197
+ annotate: 'There is no artifact $(ART), use $(CODE) instead',
198
+ // do not mention 'extend context', that is not in CAPire
199
+ service: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) or $(KEYWORD) instead',
200
+ });
201
+ }
202
+ // TODO: Use similar checks for EXTEND ENTITY etc - 'ext-ignoring-kind'
235
203
  }
236
- // TODO: think about 'ext-unexpected-type-argument'
237
- error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
238
- { '#': (isBuiltin ? 'indirect' : 'new-prop'), prop } );
239
- return 0;
240
- }
241
- const artVal = art[prop].val;
242
- const extVal = ext[prop].val;
243
- if (prop === 'srid') {
244
- error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'prop', prop } );
245
- }
246
- else if (typeof artVal !== 'number' || typeof extVal !== 'number' ) {
247
- // Users can't change from/to string value for property,
248
- // e.g. `variable`/`floating` for Decimal
249
- // TODO: Shouldn't the text distinguish between orig string and extension string?
250
- // Not sure whether to talk about strings if we have a keyword in CDL
251
- error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'string', prop } );
252
- }
253
- else if (extVal < artVal + (scaleDiff || 0)) {
254
- const number = artVal + (scaleDiff || 0);
255
- error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
256
- // eslint-disable-next-line object-curly-newline
257
- { '#': (scaleDiff ? 'scale' : 'number'), prop, number, otherprop: 'scale' } );
258
204
  }
259
- else {
260
- art[prop] = ext[prop];
261
- return extVal - artVal;
262
- }
263
- return 0;
264
- }
265
-
266
- function allowsTypeArgument( art, prop ) {
267
- const { parameters } = art._effectiveType;
268
- if (!parameters)
269
- return false;
270
- return parameters.includes( prop ) || parameters[0]?.name === prop;
271
205
  }
272
206
 
273
- function moveDictExtensions( art, extensionsMap, artProp, extProp = artProp ) {
274
- // TODO: setExpandStatusAnnotate
275
- const extensions = extensionsMap[extProp];
276
- if (!extensions)
277
- return;
278
- const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate
279
-
280
- for (const ext of extensions) {
281
- const extDict = ext[extProp];
282
- for (const name in extDict) {
283
- let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement
284
- const elemExt = extDict[name];
285
- if (elemExt.kind !== 'annotate' && elemExt.kind !== 'extend') // TODO: specified elems
286
- continue; // definitions inside extend, already handled
287
- dictCheck = dictCheck && checkRemainingMemberExtensions( art, elemExt, artProp, name );
288
- const elem = artDict[name] || annotateFor( art, extProp, name );
289
- setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null) );
290
- pushToDict( elem, '_extensions', elemExt );
207
+ // TODO: if extensions has more than one of returns,items,elements,enum, delete all those props
208
+ function transformArtifactExtensions( art ) {
209
+ const hasOnlySubExtensions = art._outer; // items, anonymous aspects
210
+ const dict = Object.create(null);
211
+ for (const ext of art._extensions) {
212
+ for (const prop in ext) {
213
+ if (ext[prop] === undefined) // deleted propery
214
+ continue;
215
+ // TODO: do this check nicer (after complete move to new extensions mechanism)
216
+ if (prop.charAt(0) === '@' || prop === 'doc' ||
217
+ prop === 'includes' || prop === 'columns' ||
218
+ prop === 'length' || prop === 'scale' || prop === 'precision' || prop === 'srid') {
219
+ if (!hasOnlySubExtensions)
220
+ pushToDict( dict, prop, ext );
221
+ }
222
+ else if (prop === 'elements' || prop === 'enum' || prop === 'actions' ||
223
+ prop === 'params' || prop === 'returns') {
224
+ if (ext.kind === 'extend')
225
+ pushToDict( dict, 'includes', ext );
226
+ pushToDict( dict, prop, ext );
227
+ }
291
228
  }
292
229
  }
230
+ art._extensions = dict;
293
231
  }
294
232
 
295
- // function moveReturnsExtensions( art, extensionsMap ) {
296
- // const artReturns = art.returns;
297
- // const extensions = extensionsMap.returns;
298
- // // TODO: artItem is null
299
- // for (const ext of extensions)
300
- // pushToDict( artReturns, '_extensions', ext.returns );
301
- // }
302
-
303
- function annotateFor( art, prop, name ) {
304
- const base = annotateBase( art );
305
- if (name === '' && prop === 'params')
306
- return base.returns || annotateCreate( base, name, base, 'returns' );
307
- const dict = base[prop] || (base[prop] = Object.create( null ));
308
- if (name == null)
309
- return dict;
310
- return dict[name] || annotateCreate( dict, name, base );
233
+ /**
234
+ * Sort sources according to the reversed layered extension order without
235
+ * reporting any messages.
236
+ *
237
+ * The order of the CSN property `$sources` (from XSN `_sortedSources`) is
238
+ * defined as follows: for _any_ model
239
+ *
240
+ * - add `type $Sources: String @(Names: []);` to one of the source files
241
+ * - add `annotate $Sources with @Names: [..., ‹sourceName›]` to each source
242
+ * file where ‹sourceName› is the file name of the source
243
+ * - then the array value of `‹csn›.$sources` is the reverse of the array value
244
+ * of `‹csn›.definitions.$Sources.@Names`
245
+ */
246
+ function sortModelSources() {
247
+ const scheduled = [];
248
+ const layered = layeredExtensions( Object.values( model.sources ) );
249
+ for (;;) {
250
+ const { highest } = extensionsOfHighestLayers( layered );
251
+ if (!highest.length)
252
+ break;
253
+ highest.reverse();
254
+ scheduled.push( ...highest );
255
+ }
256
+ setLink( model, '_sortedSources', scheduled );
311
257
  }
312
258
 
313
- function annotateBase( art ) {
314
- while (art._outer) // TOOD: think about anonymous target aspect
315
- art = art._outer;
316
- // if (art._annotateS)
317
- // return art._annotateS;
318
- if (art.kind === 'annotate')
319
- return art;
259
+ function applyAllExtensions( art ) {
260
+ const extensions = art._extensions;
261
+ for (const prop in extensions) {
262
+ // TODO: do the following `if` in a nicer way
263
+ if ([ 'elements', 'enum', 'actions', 'params', 'returns' ].includes( prop ))
264
+ continue; // currently just annotates on sub elements - TODO: error here
265
+ // annotations, `doc`, `includes`, `columns`, `length`, ...
266
+ const scheduled = [];
267
+ // sort extensions according to layer (specified elements are bottom layer):
268
+ const layered = layeredExtensions( extensions[prop] );
320
269
 
321
- // TODO: more to do if annotate can have `returns` property
322
- if (art.kind === 'select')
323
- art = art._parent;
324
- if (art._main)
325
- return annotateFor( art._parent, kindProperties[art.kind].dict, art.name.id );
270
+ let cont = true;
271
+ while (cont) {
272
+ const { highest, issue } = extensionsOfHighestLayers( layered );
273
+ // console.log( 'CA:', annoName, issue, extensions)
274
+ let index = highest.length;
275
+ cont = !!index; // safety
276
+ while (--index >= 0) {
277
+ const ext = highest[index];
278
+ scheduled.push( ext );
279
+ if (extensionOverwrites( ext, prop )) {
280
+ cont = false;
281
+ break;
282
+ }
283
+ }
284
+ if (issue || index > 0)
285
+ reportDuplicateExtensions( highest, prop, issue, index, art );
286
+ }
287
+ // Now apply the relevant extensions
288
+ scheduled.reverse();
289
+ for (const ext of scheduled)
290
+ applySingleExtension( art, ext, prop );
291
+ delete extensions[prop];
292
+ }
293
+ }
326
294
 
327
- const { absolute } = art.name;
328
- return model.$lateExtensions[absolute] || annotateCreate( model.$lateExtensions, absolute );
295
+ function extensionOverwrites( ext, prop ) {
296
+ return (prop.charAt(0) !== '@')
297
+ ? [ 'doc', 'length', 'precision', 'scale', 'srid' ].includes( prop )
298
+ : !annotationHasEllipsis( ext[prop] );
329
299
  }
330
300
 
331
- function annotateCreate( dict, id, parent, prop ) {
332
- const annotate = {
333
- kind: 'annotate',
334
- name: { id, location: genLocation },
335
- $inferred: '',
336
- location: genLocation,
337
- };
338
- if (parent) {
339
- setLink( annotate, '_parent', parent );
340
- setLink( annotate, '_main', parent._main || parent );
341
- }
342
- else {
343
- annotate.name.absolute = id; // TODO later (if all names are sparse): delete absolute
344
- }
345
- dict[prop || id] = annotate;
346
- return annotate;
347
- }
348
-
349
- function extendHandleReturns( extensions, art ) {
350
- for (const ext of extensions || []) {
351
- if (ext.$syntax === 'returns') { // TODO tmp: no proper XSN representation
352
- ext.$syntax = '$inside-returns';
353
- delete ext.params;
354
- }
355
- else {
356
- warning( 'ext-expected-returns', [ ext.name.location, ext ], {
357
- '#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',
358
- }, {
359
- std: 'Expected $(CODE)', // unused variant
360
- action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)',
361
- function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
362
- } );
363
- }
364
- }
365
- }
366
-
367
- // const unexpected_props = {
368
- // elements: 'anno-unexpected-elements',
369
- // enum: 'anno-unexpected-elements', // TODO
370
- // params: 'anno-unexpected-params',
371
- // actions: 'anno-unexpected-actions',
372
- // };
373
- // const undefined_props = {
374
- // elements: 'anno-undefined-element',
375
- // enum: 'anno-undefined-element', // TODO
376
- // params: 'anno-undefined-param',
377
- // actions: 'anno-undefined-action',
378
- // };
379
-
380
- function checkRemainingMemberExtensions( parent, ext, prop, name ) {
381
- // console.log('CRME:',prop,name,parent,ext)
382
- const dict = parent[prop];
383
- if (!dict) {
384
- // TODO: check - for each name? - better locations
385
- const location = ext._parent[prop][$location] || ext.name.location;
386
- // Remark: no `elements` dict location with `annotate Main:elem`
387
- switch (prop) {
388
- // TODO: change texts, somehow similar to checkDefinitions() ?
389
- case 'elements':
390
- case 'enum': // TODO: extra?
391
- warning( 'anno-unexpected-elements', [ location, ext._parent ],
392
- { '#': (parent._effectiveType?.kind === 'entity') ? 'entity' : 'std' }, {
393
- std: 'Elements only exist in entities, types or typed constructs',
394
- entity: 'Elements of entity types can\'t be annotated',
395
- });
396
- break;
397
- case 'params':
398
- warning( 'anno-unexpected-params', [ location, ext._parent ], {},
399
- 'Parameters only exist for actions or functions' );
400
- break;
401
- case 'actions':
402
- warning( 'anno-unexpected-actions', [ location, ext._parent ], {},
403
- 'Actions and functions only exist top-level and for entities' );
404
- break;
405
- default:
406
- // assert
407
- }
408
- return false;
409
- }
410
- else if (!dict[name]) {
411
- // TODO: make variant `returns` an auto-variant for ($ART) ?
412
- const inReturns = parent._parent?.returns && parent._parent;
413
- const art = inReturns || parent;
414
- switch (prop) {
415
- case 'elements':
416
- notFound( 'anno-undefined-element', ext.name.location, ext,
417
- { '#': (inReturns ? 'returns' : 'element'), art, name },
418
- parent.elements );
419
- break;
420
- case 'enum': // TODO: extra msg id?
421
- notFound( 'anno-undefined-element', ext.name.location, ext,
422
- { '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
423
- parent.enum );
424
- break;
425
- case 'params':
426
- notFound( 'anno-undefined-param', ext.name.location, ext,
427
- { '#': 'param', art: parent, name },
428
- parent.params );
429
- break;
430
- case 'actions':
431
- notFound( 'anno-undefined-action', ext.name.location, ext,
432
- { '#': 'action', art: parent, name },
433
- parent.actions );
434
- break;
435
- default:
436
- // assert
437
- }
438
- }
439
- return true;
440
- }
441
-
442
- function notFound( msgId, location, address, args, validDict ) {
443
- const msg = message( msgId, [ location, address ], args );
444
- attachAndEmitValidNames( msg, validDict );
445
- }
446
-
447
- // For createRemainingAnnotateStatements(): -----------------------------------
448
-
449
- function createSuperAnnotate( annotate ) {
450
- const extensions = annotate._extensions;
451
- if (extensions && !annotate._main) {
452
- const { absolute } = annotate.name;
453
- const isLocalized = absolute.startsWith( 'localized.' ); // TODO: && anno
454
- const art = model.definitions[absolute];
455
- for (const ext of extensions)
456
- checkRemainingMainExtensions( art, ext, isLocalized );
457
- if (art?.builtin && art.kind !== 'namespace') { // TODO: do not set `builtin` on cds, cds.hana
458
- setLink( annotate, '_extensions', art._extensions ); // for messages and member extensions
459
- // direct annotations on builtins or on the builtins for propagation, and
460
- // also shallow-copied to $collectedExtensions for to-csn
461
- for (const prop in art) {
462
- if (prop.charAt(0) === '@' || prop === 'doc')
463
- annotate[prop] = art[prop];
464
- }
465
- }
466
- if (extensions.length === 1) { // i.e. no proper location if from more than one extensions
467
- annotate.location = extensions[0].location;
468
- annotate.name.location = extensions[0].name.location;
469
- }
470
- }
471
- chooseAnnotationsInArtifact( annotate );
472
- extendArtifactAfter( annotate );
473
- forEachMember( annotate, createSuperAnnotate );
474
- }
475
-
476
- function checkRemainingMainExtensions( art, ext, localized ) {
477
- if (localized) // TODO v4: ignore only for annotate
478
- return;
479
- if (!resolvePath( ext.name, ext.kind, ext )) // error for extend, info for annotate
480
- return;
481
- // else if (ext.kind === 'extend') { // TODO v4 - add error
482
- // }
483
- if (art?.kind === 'namespace') {
484
- // TODO: not at all different to having no definition
485
- info( 'anno-namespace', [ ext.name.location, ext ], {}, // TODO: better location?
486
- 'Namespaces can\'t be annotated' );
487
- }
488
- else if (art?.builtin) {
489
- info( 'anno-builtin', [ ext.name.location, ext ], {}, // TODO: better location?
490
- 'Builtin types should not be annotated. Use custom type instead' );
491
- }
492
- }
493
-
494
- // Issue messages for annotations on namespaces and builtins
495
- // (TODO: really here?, probably split main artifacts vs returns)
496
- // see also lateExtensions() where similar messages are reported
497
- function checkAnnotate( construct, art ) {
498
- // TODO: Handle extend statements properly: Different message for empty extend?
499
-
500
- // --> without art._block, art not found
501
- if (construct.kind === 'annotate' && art._block?.$frontend === 'cdl') {
502
- if (construct.$syntax === 'returns' && art.kind !== 'action' && art.kind !== 'function' ) {
503
- // `annotate ABC with returns {}` is handled just like `elements`. Warn if it is used
504
- // for non-actions. We can't only check for !art.returns, because `action A();` is valid.
505
- // `art._block` ensures that `art` is a defined def.
506
- return;
507
- // warning('ext-unexpected-returns', [ construct.name.location, construct ],
508
- // { keyword: 'returns', meta: art.kind }, 'Unexpected $(KEYWORD) for $(META)');
509
- }
510
- else if (construct.$syntax !== 'returns' &&
511
- (art.kind === 'action' || art.kind === 'function') && construct.elements) {
512
- warning('ext-expected-returns', [ construct.name.location, construct ], {
513
- '#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',
514
- }, {
515
- std: 'Expected $(CODE)', // unused variant
516
- action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)',
517
- function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
518
- });
519
- }
520
- }
521
- }
522
-
523
- // extend ------------------------------------------------------------------
524
-
525
- /**
526
- * Apply the extensions inside the extensionsDict on the model.
527
- *
528
- * First try normally: extends with structure includes; with remaining cyclic
529
- * includes, do so without includes.
530
- */
531
- function applyExtensions() {
532
- let noIncludes = false;
533
- let extNames = Object.keys( extensionsDict ).sort();
534
-
535
- while (extNames.length) {
536
- const { length } = extNames;
537
- for (const name of extNames) {
538
- const art = model.definitions[name];
539
- if (art && art.kind !== 'namespace' &&
540
- extendArtifact( extensionsDict[name], art, noIncludes ))
541
- delete extensionsDict[name];
542
- }
543
- extNames = Object.keys( extensionsDict ); // no sort() required anymore
544
- if (extNames.length >= length)
545
- noIncludes = Object.keys( extensionsDict ); // = no includes
546
- }
547
- }
548
-
549
- function checkExtensionsKind( extensions, art ) {
550
- for (const ext of extensions) {
551
- const kind = ext.expectedKind?.val;
552
- if (kind && kind !== art.kind) {
553
- const loc = ext.expectedKind.location;
554
- if (kind === 'context' || kind === 'service') {
555
- // We have no real artifact during the construction of a super-annotate statement:
556
- const msgArgs = {
557
- '#': (art.kind === 'service' || art.kind === 'annotate') ? art.kind : 'std',
558
- art,
559
- kind,
560
- code: 'extend … with definitions',
561
- keyword: 'extend service',
562
- };
563
- warning( 'ext-invalid-kind', [ loc, ext ], msgArgs, {
564
- std: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) instead',
565
- annotate: 'There is no artifact $(ART), use $(CODE) instead',
566
- // do not mention 'extend context', that is not in CAPire
567
- service: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) or $(KEYWORD) instead',
568
- });
569
- }
570
- // TODO: Use similar checks for EXTEND ENTITY etc - 'ext-ignoring-kind'
571
- }
572
- }
573
- }
574
-
575
- /**
576
- * Extend artifact `art` by `extensions`. `noIncludes` can have values:
577
- * - false: includes are applied, extend and annotate is performed
578
- * - true: includes are not applied, extend and annotate is performed
579
- * - 'gen': no includes and no extensions allowed, annotate is performed
580
- *
581
- * @param {XSN.Extension[]} extensions
582
- * @param {XSN.Definition} art
583
- * @param {boolean|'gen'} [noIncludes=false]
584
- */
585
- function extendArtifact( extensions, art, noIncludes = false ) {
586
- if (!noIncludes && !(canApplyIncludes( art, art ) &&
587
- extensions.every( ext => canApplyIncludes(ext, art) )))
588
- return false;
589
- if (Array.isArray( noIncludes )) {
590
- canApplyIncludes( art, art, noIncludes );
591
- extensions.forEach( ext => canApplyIncludes( ext, art, noIncludes ) );
592
- }
593
- else if (!noIncludes &&
594
- !(canApplyIncludes( art, art ) &&
595
- extensions.every( ext => canApplyIncludes( ext, art) ))) {
596
- // console.log( 'FALSE:',art.name, extensions.map( e => e.name ) )
597
- return false;
598
- }
599
- if (!art.query) {
600
- model._entities.push( art ); // add structure with includes in dep order
601
- art.$entity = ++model.$entity;
602
- }
603
- if (!noIncludes && art.includes)
604
- applyIncludes( art, art );
605
- // checkExtensionsKind( extensions, art );
606
- extendMembers( extensions, art, noIncludes === 'gen' );
607
- if (!noIncludes && art.includes) {
608
- // early propagation of specific annotation assignments
609
- propagateEarly( art, '@cds.autoexpose' );
610
- propagateEarly( art, '@fiori.draft.enabled' );
611
- }
612
- // TODO: complain about element extensions inside projection
613
- return true;
614
- }
615
-
616
- function extendMembers( extensions, art, noExtend ) {
617
- // TODO: do the whole extension stuff lazily if the elements are requested
618
- const elemExtensions = [];
619
- if (art._main) // extensions already sorted for main artifacts
620
- extensions.sort( layers.compareLayer );
621
- // TODO: use same sequence as in chooseAssignment() - better: use common code with that fn
622
- // console.log('EM:',art.name,extensions,art._extensions)
623
- for (const ext of extensions) { // those in extMap.includes
624
- // console.log(message( 'id', [ext.location, ext], { art: ext.name._artifact },
625
- // 'Info', 'EXT').toString())
626
- if (ext.name._artifact === undefined) { // not already applied
627
- setArtifactLink( ext.name, art );
628
- if (noExtend && ext.kind === 'extend') {
629
- error( 'extend-for-generated', [ ext.name.location, ext ], { art },
630
- 'You can\'t use EXTEND on the generated $(ART)' );
631
- continue;
632
- }
633
- if (ext.includes) {
634
- // TODO: currently, re-compiling from gensrc does not give the exact
635
- // element sequence - we need something like
636
- // includes = ['Base1',3,'Base2']
637
- // where 3 means adding the next 3 elements before applying include 'Base2'
638
- if (art.includes)
639
- art.includes.push(...ext.includes);
640
- else
641
- art.includes = [ ...ext.includes ];
642
- applyIncludes( ext, art );
643
- }
644
- // console.log(ext,art)
645
- checkAnnotate( ext, art );
646
- // TODO: do we allow to add elements with array of {...}? If yes, adapt
647
- initMembers( ext, art, ext._block ); // might set _extend, _annotate
648
- dependsOnSilent(art, ext); // art depends silently on ext (inverse to normal dep!)
649
- }
650
- for (const name in ext.elements) {
651
- const elem = ext.elements[name];
652
- if (elem.kind === 'element') { // i.e. not extend or annotate
653
- elemExtensions.push( elem );
654
- break; // more than one elem in same EXTEND is fine
655
- }
656
- }
657
- }
658
- if (elemExtensions.length > 1)
659
- reportUnstableExtensions( elemExtensions );
660
-
661
- // This whole function will be removed with a next change - no need to have nice code here:
662
- const extsTmp = { elements: Object.create(null), actions: Object.create(null) };
663
- for (const e of extensions) {
664
- for (const n in e.elements || []) {
665
- if (e.elements[n].kind === 'extend')
666
- pushToDict( extsTmp.elements, n, e.elements[n] );
667
- }
668
- for (const n in extensions.actions || []) {
669
- if (e.actions[n].kind === 'extend')
670
- pushToDict( extsTmp.actions, n, e.actions[n] );
671
- }
672
- }
673
- [ 'elements', 'actions' ].forEach( (prop) => {
674
- const dict = extsTmp[prop];
675
- for (const name in dict) {
676
- let obj = art;
677
- if (obj.targetAspect)
678
- obj = obj.targetAspect;
679
- while (obj.items)
680
- obj = obj.items;
681
- const validDict = obj[prop] || prop === 'elements' && obj.enum;
682
- const member = validDict && validDict[name];
683
- if (!member)
684
- extendNothing( dict[name], prop, name, art, validDict );
685
- else if (!(member.$duplicates))
686
- extendMembers( dict[name], member );
687
- }
688
- });
689
- }
690
-
691
- /**
692
- * Check that special `sap.common.*` aspects for `.texts` entities are
693
- * consistent with compiler expectations. Emits messages and returns
694
- * false if the aspects are not valid.
695
- *
696
- * @return {boolean}
697
- */
698
- function checkTextsAspect() {
699
- const textsAspect = model.definitions['sap.common.TextsAspect'];
700
- if (!textsAspect)
701
- return false;
702
-
703
- const specialElements = { locale: { key: true } };
704
-
705
- if (textsAspect.kind !== 'aspect' || !textsAspect.elements) {
706
- error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
707
- { '#': 'no-aspect', art: textsAspect });
708
- return false;
709
- }
710
-
711
- let hasError = false;
712
- if (addTextsLanguageAssoc && textsAspect.elements.language) {
713
- const lang = textsAspect.elements.language;
714
- error('def-unexpected-element', [ lang.name.location, lang ],
715
- { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
716
- // eslint-disable-next-line max-len
717
- '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
718
- hasError = true;
719
- }
720
-
721
- for (const name in specialElements) {
722
- const expected = specialElements[name];
723
- const elem = textsAspect.elements[name];
724
- if (!elem) {
725
- error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
726
- { '#': 'missing', art: textsAspect, name });
727
- hasError = true;
728
- }
729
- else if (expected.key !== undefined && !!elem.key?.val !== expected.key) {
730
- const loc = elem.key?.location || elem.name?.location || textsAspect.name.location;
731
- error('def-invalid-texts-aspect', [ loc, elem ],
732
- { '#': expected.key ? 'key' : 'no-key', art: elem });
733
- hasError = true;
734
- }
735
- }
736
-
737
- if (hasError) // avoid subsequent errors, if the special elements are already wrong
738
- return false;
739
-
740
- for (const name in textsAspect.elements) {
741
- const elem = textsAspect.elements[name];
742
- const include = elem.$inferred === 'include';
743
- if (!specialElements[name] && elem.key) {
744
- const loc = include ? elem.location : elem.key.location;
745
- error( 'def-unexpected-key', [ loc, elem ],
746
- { '#': !include ? 'std' : 'include', art: textsAspect } );
747
- hasError = true;
748
- }
749
- else if (hasTruthyProp( elem, 'localized' )) {
750
- // TODO: T:loc, i.e. "localized" from other type (needs resolver?)
751
- // Not supported anyway, but important for recompilation (which fails correctly).
752
- const loc = elem.localized?.location || elem.location;
753
- error( 'def-unexpected-localized', [ loc, elem ],
754
- { '#': !include ? 'std' : 'include', art: textsAspect } );
755
- hasError = true;
756
- }
757
- else if (elem.targetAspect) {
758
- error( 'def-unexpected-composition', [ elem.targetAspect.location, elem ],
759
- { art: textsAspect },
760
- '$(ART) can\'t have composition of aspects' );
761
- hasError = true;
762
- }
763
- }
764
-
765
- return !hasError;
766
- }
767
-
768
- /**
769
- * Report 'Warning: Unstable element order due to repeated extensions'
770
- * except if all extensions are in the same file.
771
- *
772
- * @param {XSN.Extension[]} extensions
773
- */
774
- function reportUnstableExtensions( extensions ) {
775
- // No message if all extensions are in the same file:
776
- const file = layers.realname( extensions[0] );
777
- if (extensions.every( ( ext, i ) => !i || file === layers.realname( ext ) ))
778
- return;
779
- // Similar to chooseAssignment(), TODO there: also extra intralayer message
780
- // as this is a modeling error
781
- let lastExt = null;
782
- let open = []; // the "highest" layers
783
- for (const ext of extensions) {
784
- const extLayer = layers.layer( ext ) || { realname: '', _layerExtends: Object.create(null) };
785
- if (!open.length) {
786
- lastExt = ext;
787
- open = [ extLayer.realname ];
788
- }
789
- else if (extLayer.realname === open[open.length - 1]) { // in same layer
790
- if (lastExt) {
791
- message( 'extend-repeated-intralayer', [ lastExt.location, lastExt ] );
792
- lastExt = null;
793
- }
794
- message( 'extend-repeated-intralayer', [ ext.location, ext ] );
795
- }
796
- else {
797
- if (lastExt && (open.length > 1 || !extLayer._layerExtends[open[0]])) {
798
- // report for lastExt if that is unrelated to other open exts or current ext
799
- message( 'extend-unrelated-layer', [ lastExt.location, lastExt ], {},
800
- 'Unstable element order due to other extension in unrelated layer' );
801
- }
802
- lastExt = ext;
803
- open = open.filter( name => !extLayer._layerExtends[name] );
804
- open.push( extLayer.realname );
805
- }
806
- }
807
- }
808
-
809
-
810
- /**
811
- * @param {XSN.Extension[]} extensions
812
- * @param {string} prop
813
- * @param {string} name
814
- * @param {XSN.Artifact} art
815
- * @param {object} validDict
816
- */
817
- function extendNothing( extensions, prop, name, art, validDict ) {
818
- const artName = searchName( art, name, dictKinds[prop] );
819
- for (const ext of extensions) {
820
- // TODO: use shared functionality with notFound in resolver.js
821
- const { location } = ext.name;
822
- const extName = { ...artName, kind: ext.kind };
823
- const msg
824
- = error( 'extend-undefined', [ location, extName ],
825
- { art: artName },
826
- {
827
- std: 'Unknown $(ART) - nothing to extend',
828
- // eslint-disable-next-line max-len
829
- element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend',
830
- action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
831
- } );
832
- attachAndEmitValidNames(msg, validDict);
833
- }
834
- }
835
-
836
- // includes ----------------------------------------------------------------
837
-
838
- /**
839
- * Returns true, if `art.includes` can be applied on `target`.
840
- * They can't be applied if any of the artifacts referenced in
841
- * `art.includes` are yet to be extended.
842
- * `art !== target` if `art` is an extension.
843
- *
844
- * @param {XSN.Definition} art
845
- * @param {XSN.Artifact} target
846
- * @returns {boolean}
847
- */
848
- function canApplyIncludes( art, target, justResolveCyclic ) {
849
- if (!art.includes)
850
- return true;
851
- const isView = !!target.query;
852
- for (const ref of art.includes) {
853
- const name = resolveUncheckedPath( ref, 'include', art );
854
- // console.log('CAI:',justResolveCyclic, name, ref.path, Object.keys(extensionsDict))
855
- if (justResolveCyclic) {
856
- if (!justResolveCyclic.includes( name ))
857
- continue;
858
- delete ref._artifact;
859
- }
860
- else if (name && name in extensionsDict) {
861
- // one of the includes has itself extensions that need to be applied first
862
- return false;
863
- }
864
- else if (ref._artifact) {
865
- delete ref._artifact;
866
- }
867
- resolvePath( ref, isView ? 'viewInclude' : 'include', art );
868
- }
869
- return true;
870
- }
871
-
872
- /**
873
- * Apply all includes of `ext` on `ext`. Checks that `art` allows includes.
874
- * If `ext === art`, then includes of the artifact itself are applied.
875
- * If `ext !== art`, applies includes on the extensions, not artifact.
876
- * Sets `_ancestor` links on `art`.
877
- *
878
- * Examples:
879
- * ext === art: `entity E : F {}` => add elements of F to E
880
- * ext !== art: `extend E with F` => add elements of F to extension on E
881
- *
882
- * @param {XSN.Extension} ext
883
- * @param {XSN.Artifact} art
884
- */
885
- function applyIncludes( ext, art ) {
886
- if (kindProperties[art.kind].include !== true) {
887
- error('extend-unexpected-include', [ ext.includes[0]?.location, ext ],
888
- { meta: art.kind });
889
- return;
890
- }
891
-
892
- if (!art.query) {
893
- if (!art._ancestors)
894
- setLink( art, '_ancestors', [] ); // recursive array of includes
895
- for (const ref of ext.includes) {
896
- const template = ref._artifact;
897
- // !template -> non-includable, e.g. scalar type, or cyclic
898
- if (template) {
899
- if (template._ancestors)
900
- art._ancestors.push( ...template._ancestors );
901
- art._ancestors.push( template );
902
- }
903
- }
904
- }
905
- if (!art.query) // do not set art.elements and art.enums with query entity!
906
- includeMembers( ext, art, 'elements' );
907
- includeMembers( ext, art, 'actions' );
908
- }
909
-
910
- /**
911
- * Add all members (e.g. elements or actions) of `ext.includes` to `ext[prop]`.
912
- * If `art` is `ext`, set the parent link accordingly.
913
- *
914
- * @param {XSN.Extension} ext
915
- * @param {XSN.Artifact} art
916
- * @param {string} prop: 'elements' or 'actions'
917
- */
918
- function includeMembers( ext, art, prop ) {
919
- // TODO two kind of messages:
920
- // Error 'More than one include defines element "A"' (at include ref)
921
- // Warning 'Overwrites definition from include "I" (at elem def)
922
- const parent = ext === art && art;
923
- const members = ext[prop];
924
- ext[prop] = Object.create(null); // TODO: do not set actions property if there are none
925
- for (const ref of ext.includes) {
926
- const template = ref._artifact; // already resolved
927
- if (template) { // be robust
928
- forEachInOrder( template, prop, ( origin, name ) => {
929
- if (members && name in members)
930
- return; // TODO: warning for overwritten element
931
- const elem = linkToOrigin( origin, name, parent, prop, weakLocation( ref.location ) );
932
- if (!parent) // not yet set for EXTEND foo WITH bar => linkToOrigin() did not add it
933
- dictAdd( ext[prop], name, elem );
934
- elem.$inferred = 'include';
935
- if (origin.masked)
936
- elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );
937
- if (origin.key)
938
- elem.key = Object.assign( { $inferred: 'include' }, origin.key );
939
- if (origin.value && origin.$syntax === 'calc') {
940
- // TODO: If paths become invalid in the new artifact, should we mark
941
- // all usages in the expressions? Possibly just the first one?
942
- elem.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
943
- elem.$syntax = 'calc';
944
- setLink( elem, '_calcOrigin', origin._calcOrigin || origin );
945
- }
946
- // TODO: also complain if elem is just defined in art
947
- });
948
- }
949
- }
950
- checkRedefinitionThroughIncludes( parent, prop );
951
- // TODO: expand elements having direct elements (if needed)
952
- if (members) {
953
- forEachInOrder( { [prop]: members }, prop, ( elem, name ) => {
954
- dictAdd( ext[prop], name, elem );
955
- });
956
- }
957
- }
958
-
959
- /**
960
- * Report duplicates in parent[prop] that happen due to multiple includes having the
961
- * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
962
- *
963
- * TODO(v4): Make this a hard error; see checkRedefinition(); maybe combine both;
964
- */
965
- function checkRedefinitionThroughIncludes( parent, prop ) {
966
- if (!parent[prop])
967
- return;
968
- forEachInOrder(parent, prop, ( member, name ) => {
969
- if (member.$inferred === 'include' && Array.isArray(member.$duplicates)) {
970
- const includes = [ member, ...member.$duplicates ].map(dup => dup._origin._main);
971
- if (isBetaEnabled(options, 'v4preview')) {
972
- error( 'duplicate-definition', [ parent.name.location, member ],
973
- { '#': `include-${ prop }`, name, sorted_arts: includes } );
974
- }
975
- else {
976
- // Error accidentally removed in v2/v3, therefore only a warning.
977
- warning( 'ref-duplicate-include-member', [ parent.name.location, member ],
978
- { '#': prop, name, sorted_arts: includes } );
979
- }
980
- }
981
- });
982
- }
983
-
984
- // localized texts entities
985
-
986
- /**
987
- * Process localized data for `art`. This includes creating `.texts` entities
988
- * and `locale` associations.
989
- *
990
- * @param {XSN.Artifact} art
991
- */
992
- function processLocalizedData( art ) {
993
- const fioriAnno = art['@fiori.draft.enabled'];
994
- const fioriEnabled = fioriAnno && (fioriAnno.val === undefined || fioriAnno.val);
995
-
996
- const textsName = `${ art.name.absolute }.texts`;
997
- const textsEntity = model.definitions[textsName];
998
- const localized = localizedData( art, textsEntity, fioriEnabled );
999
- if (!localized)
1000
- return;
1001
- if (textsEntity) // expanded localized data in source
1002
- return; // -> make it idempotent
1003
- createTextsEntity( art, textsName, localized, fioriEnabled );
1004
- addTextsAssociations( art, textsName, localized );
1005
- }
1006
-
1007
- /**
1008
- * Returns `false`, if there is no localized data or an array of elements
1009
- * that are required for `.texts` entities such as keys and localized elements.
1010
- *
1011
- * @param {XSN.Artifact} art
1012
- * @param {XSN.Artifact|undefined} textsEntity
1013
- * @param {boolean} fioriEnabled
1014
- * @returns {false|XSN.Element[]}
1015
- */
1016
- function localizedData( art, textsEntity, fioriEnabled ) {
1017
- let keys = 0;
1018
- const textElems = [];
1019
- const conflictingElements = [];
1020
- // These elements are required or the localized-mechanism does not work.
1021
- // Other elements from sap.common.TextsAspect may be "overridden" as per
1022
- // usual include-mechanism.
1023
- const protectedElements = [ 'locale', 'texts', 'localized' ];
1024
- if (fioriEnabled)
1025
- protectedElements.push('ID_texts');
1026
- if (addTextsLanguageAssoc)
1027
- protectedElements.push('language');
1028
-
1029
- for (const name in art.elements) {
1030
- const elem = art.elements[name];
1031
- if (elem.$duplicates)
1032
- return false; // no localized-data unfold with redefined elems
1033
- if (protectedElements.includes( name ))
1034
- conflictingElements.push( elem );
1035
-
1036
- const isKey = elem.key && elem.key.val;
1037
- const isLocalized = hasTruthyProp( elem, 'localized' );
1038
-
1039
- if (isKey) {
1040
- keys += 1;
1041
- textElems.push( elem );
1042
- }
1043
- else if (isLocalized) {
1044
- textElems.push( elem );
301
+ // TODO: still a bit annotation assignment specific
302
+ function reportDuplicateExtensions( extensions, prop, issue, index, art ) {
303
+ // TODO: think about messages for these
304
+ if (prop === 'elements' || prop === 'enum' || prop === 'actions' || prop === 'columns' ||
305
+ prop === 'params' || prop === 'returns' || prop === 'includes' )
306
+ return; // extensions currently handled extra
307
+ if (issue) {
308
+ // eslint-disable-next-line no-nested-ternary
309
+ let msg = (index < 0)
310
+ ? 'anno-unstable-array'
311
+ : (issue === true)
312
+ ? 'anno-duplicate'
313
+ : 'anno-duplicate-unrelated-layer';
314
+ if (prop.charAt(0) !== '@' && prop !== 'doc') {
315
+ msg = (issue === true)
316
+ ? 'ext-duplicate-extend-type'
317
+ : 'ext-duplicate-extend-type-unrelated-layer';
318
+ // not sure whether to repeat the extended artifact in the message (we
319
+ // have the semantic location, after all)
1045
320
  }
1046
-
1047
- if (isKey && isLocalized) { // key with localized is wrong - ignore localized
1048
- const errpos = elem.localized || elem.type || elem.name;
1049
- warning( 'def-ignoring-localized-key', [ errpos.location, elem ], { keyword: 'localized' },
1050
- 'Keyword $(KEYWORD) is ignored for primary keys' );
321
+ const variant = prop === 'doc' ? 'doc' : 'std';
322
+ for (const ext of extensions) {
323
+ const anno = ext[prop];
324
+ if (anno && !anno.$errorReported) {
325
+ message( msg, [ anno.name?.location || anno.location, ext ],
326
+ { '#': variant, anno: prop, type: art } );
327
+ }
1051
328
  }
1052
329
  }
1053
- if (textElems.length <= keys)
1054
- return false;
1055
-
1056
- if (!keys) {
1057
- warning( 'def-expecting-key', [ art.name.location, art ], {},
1058
- 'No texts entity can be created when no key element exists' );
1059
- return false;
330
+ else if (index > 0) { // more than one set (not just ...)
331
+ const variant = prop === 'doc' ? 'doc' : 'std';
332
+ const msgid = (prop.charAt(0) === '@' || prop === 'doc')
333
+ ? 'anno-duplicate-same-file' // TODO: always ext-duplicate-…
334
+ : 'ext-duplicate-same-file';
335
+ while (index >= 0) { // do not report for trailing [...]
336
+ const ext = extensions[index--];
337
+ const anno = ext[prop];
338
+ warning( msgid, [ anno.name?.location || anno.location, ext ],
339
+ { '#': variant, prop, anno: prop } );
340
+ }
1060
341
  }
342
+ }
1061
343
 
1062
- if (textsEntity) {
1063
- if (textsEntity.$duplicates)
1064
- return false;
1065
- if (textsEntity.kind !== 'entity' || textsEntity.query ||
1066
- // already have elements "texts" and "localized" (and optionally ID_texts)
1067
- conflictingElements.length !== 2 || art.elements.locale ||
1068
- (fioriEnabled && art.elements.ID_texts)) {
1069
- // TODO if we have too much time: check all elements of texts entity for safety
1070
- warning( null, [ art.name.location, art ], { art: textsEntity },
1071
- // eslint-disable-next-line max-len
1072
- 'Texts entity $(ART) can\'t be created as there is another definition with that name' );
1073
- info( null, [ textsEntity.name.location, textsEntity ], { art },
1074
- 'Texts entity for $(ART) can\'t be created with this definition' );
1075
- }
1076
- else if (!art._block || art._block.$frontend !== 'json') {
1077
- info( null, [ art.name.location, art ], {},
1078
- 'Localized data expansions has already been done' );
1079
- return textElems; // make double-compilation even with after toHana
1080
- }
1081
- else if (!art._block.$withLocalized && !options.$recompile) {
1082
- art._block.$withLocalized = true;
1083
- info( 'recalculated-text-entities', [ art.name.location, null ], {},
1084
- 'Input CSN contains expansions for localized data' );
1085
- return textElems; // make compilation idempotent
344
+ function applySingleExtension( art, ext, prop ) {
345
+ if (prop === 'includes') {
346
+ if (ext.kind === 'extend' && art.$inferred) {
347
+ error( 'extend-for-generated', [ ext.name.location, ext ], { art },
348
+ 'You can\'t use EXTEND on the generated $(ART)' );
1086
349
  }
1087
- else {
1088
- return textElems;
350
+ else if (art.kind !== 'annotate' && !art._outer) { // not with elem extension in targetAspect
351
+ const { absolute } = art.name;
352
+ const dict = extensionsDict[absolute] || (extensionsDict[absolute] = []);
353
+ dict.push( ext ); // TODO: change
354
+ // console.log( 'ASI:',prop,art.name,ext,extensionsDict[absolute])
1089
355
  }
356
+ // art[prop] = (art[prop]) ? art[prop].concat( ext[prop] ) : ext[prop];
357
+ }
358
+ else if (prop === 'columns') {
359
+ const { query } = art;
360
+ if (!query?.from?.path)
361
+ error( 'extend-columns', [ ext.columns[$location], ext ], { art } );
362
+ else if (!query.columns)
363
+ query.columns = [ { location: query.from.location, val: '*' }, ...ext.columns ];
364
+ else
365
+ query.columns.push( ...ext.columns );
366
+ }
367
+ else if ([ 'length', 'precision', 'scale', 'srid' ].includes( prop )) {
368
+ const typeExts = art.$typeExts || (art.$typeExts = {});
369
+ typeExts[prop] = ext;
1090
370
  }
1091
- for (const elem of conflictingElements) {
1092
- warning( null, [ elem.name.location, art ], { name: elem.name.id },
1093
- 'No texts entity can be created when element $(NAME) exists' );
371
+ else {
372
+ const result = applyAssignment( art[prop], ext[prop], ext, prop );
373
+ art[prop] = (result.name) ? result : Object.assign( {}, art[prop], result );
1094
374
  }
1095
- return !textsEntity && !conflictingElements.length && textElems;
1096
375
  }
1097
376
 
1098
- /**
1099
- * Create the `.texts` entity for the given base artifact.
1100
- *
1101
- * @param {XSN.Artifact} base
1102
- * @param {string} absolute
1103
- * @param {XSN.Element[]} textElems
1104
- * @param {boolean} fioriEnabled
1105
- */
1106
- function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
1107
- const art = useTextsAspect
1108
- ? createTextsEntityWithInclude( base, absolute, fioriEnabled )
1109
- : createTextsEntityWithDefaultElements( base, absolute, fioriEnabled );
1110
- // both functions are rather similar...
1111
-
1112
- const { location } = base.name;
1113
-
1114
- if (addTextsLanguageAssoc) {
1115
- const language = {
1116
- name: { location, id: 'language' },
1117
- kind: 'element',
1118
- location,
1119
- type: augmentPath( location, 'cds.Association' ),
1120
- target: augmentPath( location, 'sap.common.Languages' ),
1121
- on: {
1122
- op: { val: '=', location },
1123
- args: [
1124
- { path: [ { id: 'language', location }, { id: 'code', location } ], location },
1125
- { path: [ { id: 'locale', location } ], location },
1126
- ],
1127
- location,
1128
- },
1129
- };
1130
- setLink( language, '_block', model.$internal );
1131
- dictAdd( art.elements, 'language', language );
377
+ function applyAssignment( previousAnno, anno, art, annoName ) {
378
+ const firstEllipsis = annotationHasEllipsis( anno );
379
+ if (!firstEllipsis)
380
+ return anno;
381
+ const hasBase = previousAnno?.literal === 'array';
382
+ if (!previousAnno) {
383
+ const loc = firstEllipsis.location || anno.name.location;
384
+ message( 'anno-unexpected-ellipsis', [ loc, art ], { code: '...' } );
385
+ previousAnno = { val: [] };
1132
386
  }
1133
-
1134
- // assertUnique array value, first entry is 'locale'
1135
- const assertUniqueValue = [];
1136
-
1137
- for (const orig of textElems) {
1138
- const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
1139
- if (orig.key && orig.key.val) {
1140
- // elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location };
1141
- // TODO: the previous would be better, but currently not supported in toCDL
1142
- if (!fioriEnabled) {
1143
- elem.key = { val: true, $inferred: 'localized', location };
1144
- // If the propagated elements remain key (that is not fiori.draft.enabled)
1145
- // they should be omitted from OData containment EDM
1146
- setAnnotation( elem, '@odata.containment.ignore', location );
387
+ else if (previousAnno.literal !== 'array') {
388
+ // TODO: If we introduce sub-messages, point to the non-array base value.
389
+ error( 'anno-mismatched-ellipsis', [ anno.name.location, art ], { code: '...' } );
390
+ previousAnno = { val: [] };
391
+ }
392
+ const previousValue = previousAnno.val;
393
+ let prevPos = 0;
394
+ const result = [];
395
+ for (const item of anno.val) {
396
+ const ell = item && item.literal === 'token' && item.val === '...';
397
+ if (!ell) {
398
+ result.push( item );
399
+ }
400
+ else {
401
+ let upToSpec = item.upTo && checkUpToSpec( item.upTo, art, annoName, true );
402
+ while (prevPos < previousValue.length) {
403
+ const prevItem = previousValue[prevPos++];
404
+ result.push( prevItem );
405
+ if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) {
406
+ upToSpec = false;
407
+ break;
408
+ }
1147
409
  }
1148
- else {
1149
- // add the former key paths to the unique constraint
1150
- assertUniqueValue.push({
1151
- path: [ { id: orig.name.id, location: orig.location } ],
1152
- location: orig.location,
1153
- });
410
+ if (upToSpec && hasBase) {
411
+ // non-matched UP TO; if there is no base to apply to, there is already an error.
412
+ warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' },
413
+ 'The $(CODE) value does not match any item in the base annotation $(ANNO)' );
1154
414
  }
1155
415
  }
1156
- if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword
1157
- const localized = orig.localized || orig.type || orig.name;
1158
- elem.localized = { val: null, $inferred: 'localized', location: localized.location };
1159
- }
1160
416
  }
417
+ // console.log('TP:',previousValue.map(se),anno.val.map(se),'->',result.map(se))
418
+ return { val: result, literal: 'array' };
419
+ }
420
+ // function se(a) { return a.upTo ? [a.val,a.upTo.val] : a.val ; }
1161
421
 
1162
- initArtifact( art );
1163
- if (art.includes) {
1164
- // add elements `locale`, etc. which are required below.
1165
- applyIncludes(art, art);
422
+ function checkUpToSpec( upToSpec, art, annoName, isFullUpTo ) {
423
+ const { literal } = upToSpec;
424
+ if (!isFullUpTo) { // inside struct of UP TO
425
+ if (literal !== 'struct' && literal !== 'array' )
426
+ return true;
1166
427
  }
1167
-
1168
- if (fioriEnabled) {
1169
- // The includes mechanism puts TextsAspect's elements before .texts' elements.
1170
- // Because ID_texts is not copied from TextsAspect, the order is messed
1171
- // up. Fix it.
1172
- const { elements } = art;
1173
- art.elements = Object.create(null);
1174
- const names = [ 'ID_texts', 'locale', ...Object.keys(elements) ];
1175
- for (const name of names)
1176
- art.elements[name] = elements[name];
1177
-
1178
- const { locale } = art.elements;
1179
- assertUniqueValue.unshift({
1180
- path: [ { id: locale.name.id, location: locale.location } ],
1181
- location: locale.location,
1182
- });
1183
- setAnnotation( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' );
428
+ else if (literal === 'struct') {
429
+ return Object.values( upToSpec.struct ).every( v => checkUpToSpec( v, art, annoName ) );
1184
430
  }
1185
-
1186
- copyPersistenceAnnotations( art, base );
1187
- return art;
431
+ else if (literal !== 'array' && literal !== 'boolean' && literal !== 'null') {
432
+ return true;
433
+ }
434
+ error( null, [ upToSpec.location, art ],
435
+ { anno: annoName, code: '... up to', '#': literal },
436
+ {
437
+ std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)',
438
+ array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)',
439
+ // eslint-disable-next-line max-len
440
+ struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)',
441
+ boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)',
442
+ null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)',
443
+ } );
444
+ return false;
1188
445
  }
1189
446
 
1190
- /**
1191
- * Create the `.texts` entity for the given base artifact.
1192
- * In contrast to createTextsEntityWithDefaultElements(), this one creates
1193
- * an include for `sap.common.TextsAspect`.
1194
- *
1195
- * Does NOT apply the include!
1196
- *
1197
- * @param {XSN.Artifact} base
1198
- * @param {string} absolute
1199
- * @param {boolean} fioriEnabled
1200
- */
1201
- function createTextsEntityWithInclude( base, absolute, fioriEnabled ) {
1202
- const textsAspectName = 'sap.common.TextsAspect';
1203
- const textsAspect = model.definitions['sap.common.TextsAspect'];
1204
- const elements = Object.create(null);
1205
- const { location } = base.name;
1206
- const art = {
1207
- kind: 'entity',
1208
- name: { path: splitIntoPath( location, absolute ), absolute, location },
1209
- includes: [ createInclude( textsAspectName, base.location ) ],
1210
- location: base.location,
1211
- elements,
1212
- $inferred: 'localized-entity',
1213
- };
1214
-
1215
- if (!fioriEnabled) {
1216
- // To be compatible, we switch off draft without @fiori.draft.enabled
1217
- // TODO (next major version): remove?
1218
- setAnnotation( art, '@odata.draft.enabled', art.location, false );
447
+ function equalUpTo( previousItem, upToSpec ) {
448
+ if (!previousItem)
449
+ return false;
450
+ if ('val' in upToSpec) {
451
+ if (previousItem.val === upToSpec.val) // enum, struct and ref have no val
452
+ return true;
453
+ const typeUpTo = typeof upToSpec.val;
454
+ const typePrev = typeof previousItem.val;
455
+ if (typeUpTo === 'number')
456
+ return typePrev === 'string' && previousItem.val === upToSpec.val.toString();
457
+ if (typePrev === 'number')
458
+ return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString();
1219
459
  }
1220
- else {
1221
- // @fiori.draft.enabled artifacts need default elements ID_texts and locale.
1222
- // `locale` is copied from `sap.common.TextsAspect`, but without "key".
1223
- const textId = {
1224
- name: { location, id: 'ID_texts' },
1225
- kind: 'element',
1226
- key: { val: true, location },
1227
- type: augmentPath( location, 'cds.UUID' ),
1228
- location,
1229
- };
1230
- dictAdd( art.elements, 'ID_texts', textId );
1231
-
1232
- // "Early" include; only for element `locale`, which has its `key` property
1233
- // removed (or rather: it is not copied).
1234
- linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location );
460
+ else if (upToSpec.path) {
461
+ return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec );
1235
462
  }
1236
-
1237
- if (addTextsLanguageAssoc && art.elements.language)
1238
- art.elements.language = undefined; // TODO: Message? Ignore?
1239
- // TODO: what is this necessary? We do not create a text entity in this case
1240
-
1241
- setLink( art, '_block', model.$internal );
1242
- model.definitions[absolute] = art;
1243
- chooseAnnotationsInArtifact( art ); // having extensions here would be wrong
1244
- return art;
1245
- }
1246
-
1247
- /**
1248
- * @param {XSN.Artifact} base
1249
- * @param {string} absolute
1250
- * @param {boolean} fioriEnabled
1251
- */
1252
- function createTextsEntityWithDefaultElements( base, absolute, fioriEnabled ) {
1253
- const elements = Object.create(null);
1254
- const { location } = base.name;
1255
- const art = {
1256
- kind: 'entity',
1257
- name: { path: splitIntoPath( location, absolute ), absolute, location },
1258
- location: base.location,
1259
- elements,
1260
- $inferred: 'localized-entity',
1261
- };
1262
- // If there is a type `sap.common.Locale`, then use it as the type for the element `locale`.
1263
- // If not, use the default `cds.String` with a length of 14.
1264
- const hasLocaleType = model.definitions['sap.common.Locale']?.kind === 'type';
1265
- const locale = {
1266
- name: { location, id: 'locale' },
1267
- kind: 'element',
1268
- type: augmentPath( location, hasLocaleType ? 'sap.common.Locale' : 'cds.String' ),
1269
- location,
1270
- };
1271
- if (!hasLocaleType)
1272
- locale.length = { literal: 'number', val: 14, location };
1273
-
1274
- if (!fioriEnabled) {
1275
- locale.key = { val: true, location };
1276
- // To be compatible, we switch off draft without @fiori.draft.enabled
1277
- // TODO (next major version): remove?
1278
- setAnnotation( art, '@odata.draft.enabled', art.location, false );
463
+ else if (upToSpec.sym) {
464
+ return previousItem.sym && previousItem.sym.id === upToSpec.sym.id;
1279
465
  }
1280
- else {
1281
- const textId = {
1282
- name: { location, id: 'ID_texts' },
1283
- kind: 'element',
1284
- key: { val: true, location },
1285
- type: augmentPath( location, 'cds.UUID' ),
1286
- location,
1287
- };
1288
- dictAdd( art.elements, 'ID_texts', textId );
466
+ else if (upToSpec.struct && previousItem.struct) {
467
+ return Object.entries( upToSpec.struct )
468
+ .every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) );
1289
469
  }
1290
- dictAdd( art.elements, 'locale', locale );
1291
-
1292
- setLink( art, '_block', model.$internal );
1293
- model.definitions[absolute] = art;
1294
- chooseAnnotationsInArtifact( art ); // having extensions here would be wrong
1295
- return art;
470
+ return false;
1296
471
  }
1297
472
 
1298
- /**
1299
- * @param {XSN.Artifact} art
1300
- * @param {string} textsName
1301
- * @param {XSN.Element[]} textElems
1302
- */
1303
- function addTextsAssociations( art, textsName, textElems ) {
1304
- // texts : Composition of many Books.texts on texts.ID=ID;
1305
- /** @type {array} */
1306
- const keys = textElems.filter( e => e.key && e.key.val );
1307
- const { location } = art.name;
1308
- const texts = {
1309
- name: { location, id: 'texts' },
1310
- kind: 'element',
1311
- location,
1312
- $inferred: 'localized',
1313
- type: augmentPath( location, 'cds.Composition' ),
1314
- cardinality: { targetMax: { literal: 'string', val: '*', location }, location },
1315
- target: augmentPath( location, textsName ),
1316
- on: augmentEqual( location, 'texts', keys ),
1317
- };
1318
- setMemberParent( texts, 'texts', art, 'elements' );
1319
- setLink( texts, '_block', model.$internal );
1320
- // localized : Association to Books.texts on
1321
- // localized.ID=ID and localized.locale = $user.locale;
1322
- keys.push( [ 'localized.locale', '$user.locale' ] );
1323
- const localized = {
1324
- name: { location, id: 'localized' },
1325
- kind: 'element',
1326
- location,
1327
- $inferred: 'localized',
1328
- type: augmentPath( location, 'cds.Association' ),
1329
- target: augmentPath( location, textsName ),
1330
- on: augmentEqual( location, 'localized', keys ),
1331
- };
1332
- setMemberParent( localized, 'localized', art, 'elements' );
1333
- setLink( localized, '_block', model.$internal );
473
+ function normalizeRef( node ) { // see to-csn.js
474
+ const ref = pathName( node.path );
475
+ return node.variant ? `${ ref }#${ node.variant.id }` : ref;
1334
476
  }
1335
477
 
1336
- /**
1337
- * Create a structure that can be used as an item in `includes`.
1338
- *
1339
- * @param {string} name
1340
- * @param {XSN.Location} location
1341
- */
1342
- function createInclude( name, location ) {
1343
- const include = {
1344
- path: [ { id: name, location } ],
1345
- location,
1346
- };
1347
- setArtifactLink( include.path[0], model.definitions[name] );
1348
- setArtifactLink( include, model.definitions[name] );
1349
- return include;
1350
- }
478
+ // For extendArtifactAfter(): -------------------------------------------------
1351
479
 
1352
- /**
1353
- * Returns whether `art` directly or indirectly has the property 'prop',
1354
- * following the 'origin' and the 'type' (not involving elements).
1355
- *
1356
- * DON'T USE FOR ANNOTATIONS (see TODO below)
1357
- *
1358
- * TODO: we should issue a warning if we get localized via TYPE OF
1359
- * TODO: XSN: for anno short form, use { val: true, location, <no literal prop> }
1360
- * ...then this function also works with annotations
1361
- *
1362
- * @param {XSN.Artifact} art
1363
- * @param {string} prop
1364
- * @returns {boolean}
1365
- */
1366
- function hasTruthyProp( art, prop ) {
1367
- const processed = Object.create(null); // avoid infloops with circular refs
1368
- let name = art.name.absolute; // is ok, since no recursive type possible
1369
- while (art && !processed[name]) {
1370
- if (art[prop])
1371
- return art[prop].val;
1372
- processed[name] = art;
1373
- if (art._origin) {
1374
- art = art._origin;
1375
- if (!art.name) // anonymous aspect
1376
- return false;
1377
- name = art && art.name.absolute;
1378
- }
1379
- else if (art.type && art._block && art.type.scope !== 'typeOf') {
1380
- // TODO: also do something special for TYPE OF inside `art`s own elements
1381
- name = resolveUncheckedPath( art.type, 'type', art );
1382
- art = name && model.definitions[name];
1383
- }
1384
- else {
1385
- return false;
480
+ // Remarks on messages: we allow the type extensions only if the artifact
481
+ // originally had that property any check of the kind “type prop can only be
482
+ // used with FooBar” is independent from `extend with type`. Function
483
+ // checkTypeArguments() in resolve.js reports 'type-unexpected-argument', but
484
+ // that is currently incomplete.
485
+ //
486
+ // We then report (in the future), use the first message of:
487
+ // - the usual messages if a type argument is wrong, independently from `extend`
488
+ // - 'ext-unexpected-type-argument' (TODO) if the artifact does not have the prop
489
+ // - 'ext-invalid-type-argument' if the value is wrong for extend (no overwrite)
490
+ //
491
+ // TODO v4: do not allow `extend … with (precision: …)` alone if original def also has `scale`
492
+ function applyTypeExtensions( art, ext, prop, scaleDiff ) {
493
+ // console.log('ATE:',art?.[prop],ext?.[prop],scaleDiff)
494
+ if (!ext?.[prop])
495
+ return 0;
496
+ if (!art[prop]) {
497
+ const isBuiltin = art._effectiveType?.builtin;
498
+ if (isBuiltin && !allowsTypeArgument( art, prop )) {
499
+ // Let checkTypeArguments() in resolve.js report a message, is incomplete
500
+ // though, i.e. can only safely be used for scalars at the moment. But we
501
+ // will improve that function and not try to do extra things here.
502
+ art[prop] = ext[prop]; // enable checkTypeArguments() doing its job
503
+ return 0;
1386
504
  }
505
+ // TODO: think about 'ext-unexpected-type-argument'
506
+ error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
507
+ { '#': (isBuiltin ? 'indirect' : 'new-prop'), prop } );
508
+ return 0;
1387
509
  }
1388
- return false;
510
+ const artVal = art[prop].val;
511
+ const extVal = ext[prop].val;
512
+ if (prop === 'srid') {
513
+ error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'prop', prop } );
514
+ }
515
+ else if (typeof artVal !== 'number' || typeof extVal !== 'number' ) {
516
+ // Users can't change from/to string value for property,
517
+ // e.g. `variable`/`floating` for Decimal
518
+ // TODO: Shouldn't the text distinguish between orig string and extension string?
519
+ // Not sure whether to talk about strings if we have a keyword in CDL
520
+ error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'string', prop } );
521
+ }
522
+ else if (extVal < artVal + (scaleDiff || 0)) {
523
+ const number = artVal + (scaleDiff || 0);
524
+ error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
525
+ // eslint-disable-next-line object-curly-newline
526
+ { '#': (scaleDiff ? 'scale' : 'number'), prop, number, otherprop: 'scale' } );
527
+ }
528
+ else {
529
+ art[prop] = ext[prop];
530
+ return extVal - artVal;
531
+ }
532
+ return 0;
1389
533
  }
1390
534
 
1391
- // managed composition of aspects ------------------------------------------
535
+ function allowsTypeArgument( art, prop ) {
536
+ const { parameters } = art._effectiveType;
537
+ if (!parameters)
538
+ return false;
539
+ return parameters.includes( prop ) || parameters[0]?.name === prop;
540
+ }
1392
541
 
1393
- function processAspectComposition( base ) {
1394
- // TODO: we need to forbid COMPOSITION of entity w/o keys and ON anyway
1395
- // TODO: consider entity includes
1396
- // TODO: nested containment
1397
- // TODO: better do circular checks in the aspect!
1398
- if (base.kind !== 'entity' || base.query)
542
+ function moveDictExtensions( art, extensionsMap, artProp, extProp = artProp ) {
543
+ // TODO: setExpandStatusAnnotate
544
+ const extensions = extensionsMap[extProp];
545
+ if (!extensions)
1399
546
  return;
1400
- const keys = baseKeys();
1401
- if (keys)
1402
- forEachGeneric( base, 'elements', expand ); // TODO: recursively here?
1403
- return;
1404
-
1405
- function baseKeys() {
1406
- const k = Object.create(null);
1407
- for (const name in base.elements) {
1408
- const elem = base.elements[name];
1409
- if (elem.$duplicates)
1410
- return false; // no composition-of-type unfold with redefined elems
1411
- if (elem.key && elem.key.val)
1412
- k[name] = elem;
1413
- }
1414
- return k;
1415
- }
547
+ const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate
1416
548
 
1417
- function expand( elem ) {
1418
- if (elem.target)
1419
- return;
1420
- let origin = elem;
1421
- // included element do not have target aspect directly
1422
- while (origin && !origin.targetAspect && origin._origin)
1423
- origin = origin._origin;
1424
- let target = origin.targetAspect;
1425
- if (target && target.path)
1426
- target = resolvePath( origin.targetAspect, 'compositionTarget', origin );
1427
- if (!target || !target.elements)
1428
- return;
1429
- const entityName = `${ base.name.absolute }.${ elem.name.id }`;
1430
- const entity = allowAspectComposition( target, elem, keys, entityName ) &&
1431
- createTargetEntity( target, elem, keys, entityName, base );
1432
- elem.target = {
1433
- location: (elem.targetAspect || elem).location,
1434
- $inferred: 'aspect-composition',
1435
- };
1436
- setArtifactLink( elem.target, entity );
1437
- if (entity) {
1438
- // Support using the up_ element in the generated entity to be used
1439
- // inside the anonymous aspect:
1440
- const { up_ } = target.$tableAliases;
1441
- // TODO: invalidate "up_" alias (at least further navigation) if it
1442
- // already has an _origin (when the managed composition is included)
1443
- if (up_)
1444
- setLink( up_, '_origin', entity.elements.up_ );
1445
- model.$compositionTargets[entity.name.absolute] = true;
1446
- processAspectComposition( entity );
1447
- processLocalizedData( entity );
549
+ for (const ext of extensions) {
550
+ const extDict = ext[extProp];
551
+ for (const name in extDict) {
552
+ let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement
553
+ const elemExt = extDict[name];
554
+ if (elemExt.kind !== 'annotate' && elemExt.kind !== 'extend') // TODO: specified elems
555
+ continue; // definitions inside extend, already handled
556
+ dictCheck = dictCheck && checkRemainingMemberExtensions( art, elemExt, artProp, name );
557
+ const elem = artDict[name] || annotateFor( art, extProp, name );
558
+ setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null) );
559
+ pushToDict( elem, '_extensions', elemExt );
1448
560
  }
1449
561
  }
1450
562
  }
1451
563
 
1452
- /**
1453
- * @returns {boolean|0} `true`, if allowed, `false` if forbidden, `0` if circular containment.
1454
- */
1455
- function allowAspectComposition( target, elem, keys, entityName ) {
1456
- if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates ))
1457
- return false; // no elements or with redefinitions
1458
- const location = elem.target && elem.target.location || elem.location;
1459
- if ((elem._main._upperAspects || []).includes( target ))
1460
- return 0; // circular containment of the same aspect
1461
-
1462
- const keyNames = Object.keys( keys );
1463
- if (!keyNames.length) {
1464
- // TODO: for "inner aspect-compositions", signal already in type
1465
- error( null, [ location, elem ], { target },
1466
- 'An aspect $(TARGET) can\'t be used as target in an entity without keys' );
1467
- return false;
1468
- }
1469
- // if (keys.up_) { // only to be tested if we allow to provide a prefix, which could be ''
1470
- // // Cannot be in an "inner aspect-compositions" as it would already be wrong before
1471
- // // TODO: if anonymous type, use location of "up_" element
1472
- // // FUTURE: add sub info with location of "up_" element
1473
- // message( 'id', [location, elem], { target, name: 'up_' }, 'Error',
1474
- // 'An aspect $(TARGET) can't be used as target in an entity with a key named $(NAME)' );
1475
- // return false;
1476
- // }
1477
- if (target.elements.up_) {
1478
- // TODO: for "inner aspect-compositions", signal already in type
1479
- // TODO: if anonymous type, use location of "up_" element
1480
- // FUTURE: if named type, add sub info with location of "up_" element
1481
- error( null, [ location, elem ], { target, name: 'up_' },
1482
- 'An aspect $(TARGET) with an element named $(NAME) can\'t be used as target' );
1483
- return false;
1484
- }
1485
- if (model.definitions[entityName]) {
1486
- error( null, [ location, elem ], { art: entityName },
1487
- // eslint-disable-next-line max-len
1488
- 'Target entity $(ART) can\'t be created as there is another definition with this name' );
1489
- return false;
1490
- }
1491
- const names = Object.keys( target.elements )
1492
- .filter( n => n.startsWith('up__') && keyNames.includes( n.substring(4) ) );
1493
- if (names.length) {
1494
- // FUTURE: if named type, add sub info with location of "up_" element
1495
- error( null, [ location, elem ], { target: entityName, names }, {
1496
- std: 'Key elements $(NAMES) can\'t be added to $(TARGET) as these already exist',
1497
- one: 'Key element $(NAMES) can\'t be added to $(TARGET) as it already exist',
1498
- });
1499
- return false;
1500
- }
1501
-
1502
- if (elem.type && !isDirectComposition(elem)) {
1503
- // Only issue warning for direct usages, not for projections, includes, etc.
1504
- // TODO: Make it configurable error; v4: error
1505
- warning( 'def-expected-comp-aspect', [ elem.type.location, elem ],
1506
- { prop: 'Composition of', otherprop: 'Association to' },
1507
- 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect');
1508
- }
564
+ // function moveReturnsExtensions( art, extensionsMap ) {
565
+ // const artReturns = art.returns;
566
+ // const extensions = extensionsMap.returns;
567
+ // // TODO: artItem is null
568
+ // for (const ext of extensions)
569
+ // pushToDict( artReturns, '_extensions', ext.returns );
570
+ // }
1509
571
 
1510
- return true;
572
+ function annotateFor( art, prop, name ) {
573
+ const base = annotateBase( art );
574
+ if (name === '' && prop === 'params')
575
+ return base.returns || annotateCreate( base, name, base, 'returns' );
576
+ const dict = base[prop] || (base[prop] = Object.create( null ));
577
+ if (name == null)
578
+ return dict;
579
+ return dict[name] || annotateCreate( dict, name, base );
1511
580
  }
1512
581
 
1513
- function createTargetEntity( target, elem, keys, entityName, base ) {
1514
- const { location } = elem.targetAspect || elem.target || elem;
1515
- elem.on = {
1516
- location,
1517
- op: { val: '=', location },
1518
- args: [
1519
- augmentPath( location, elem.name.id, 'up_' ),
1520
- augmentPath( location, '$self' ),
1521
- ],
1522
- $inferred: 'aspect-composition',
1523
- };
582
+ function annotateBase( art ) {
583
+ while (art._outer) // TOOD: think about anonymous target aspect
584
+ art = art._outer;
585
+ if (art.kind === 'annotate')
586
+ return art;
1524
587
 
1525
- const elements = Object.create(null);
1526
- const art = {
1527
- kind: 'entity',
1528
- name: { path: splitIntoPath( location, entityName ), absolute: entityName, location },
1529
- location,
1530
- elements,
1531
- $inferred: 'composition-entity',
1532
- };
1533
- if (target.name) { // named target aspect
1534
- setLink( art, '_origin', target );
1535
- setLink( art, '_upperAspects', [ target, ...(elem._main._upperAspects || []) ] );
1536
- }
1537
- else {
1538
- setLink( art, '_origin', target );
1539
- // TODO: do we need to give the anonymous target aspect a kind and name?
1540
- setLink( art, '_upperAspects', elem._main._upperAspects || [] );
1541
- }
588
+ // TODO: more to do if annotate can have `returns` property
589
+ if (art.kind === 'select')
590
+ art = art._parent;
591
+ if (art._main)
592
+ return annotateFor( art._parent, kindProperties[art.kind].dict, art.name.id );
593
+
594
+ const { absolute } = art.name;
595
+ return model.$collectedExtensions[absolute] ||
596
+ annotateCreate( model.$collectedExtensions, absolute );
597
+ }
1542
598
 
1543
- const up = { // elements.up_ = ...
1544
- name: { location, id: 'up_' },
1545
- kind: 'element',
1546
- location,
1547
- $inferred: 'aspect-composition',
1548
- type: augmentPath( location, 'cds.Association' ),
1549
- target: augmentPath( location, base.name.absolute ),
1550
- cardinality: {
1551
- targetMin: { val: 1, literal: 'number', location },
1552
- targetMax: { val: 1, literal: 'number', location },
1553
- location,
1554
- },
599
+ function annotateCreate( dict, id, parent, prop ) {
600
+ const annotate = {
601
+ kind: 'annotate',
602
+ name: { id, location: genLocation },
603
+ $inferred: '',
604
+ location: genLocation,
1555
605
  };
1556
- // By default, 'up_' is a managed primary key association.
1557
- // If 'up_' shall be rendered unmanaged, infer the parent
1558
- // primary keys and add the ON condition
1559
- if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) {
1560
- addProxyElements( art, keys, 'aspect-composition', target.name && location,
1561
- 'up__', '@odata.containment.ignore' );
1562
- up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
606
+ if (parent) {
607
+ setLink( annotate, '_parent', parent );
608
+ setLink( annotate, '_main', parent._main || parent );
1563
609
  }
1564
610
  else {
1565
- up.key = { location, val: true };
1566
- // managed associations must be explicitly set to not null
1567
- // even if target cardinality is 1..1
1568
- up.notNull = { location, val: true };
611
+ annotate.name.absolute = id; // TODO later (if all names are sparse): delete absolute
1569
612
  }
1570
-
1571
- dictAdd( art.elements, 'up_', up);
1572
- addProxyElements( art, target.elements, 'aspect-composition', target.name && location );
1573
-
1574
- setLink( art, '_block', model.$internal );
1575
- model.definitions[entityName] = art;
1576
- initArtifact( art );
1577
-
1578
- chooseAnnotationsInArtifact( art ); // having extensions here would be wrong
1579
- // Copy persistence annotations from aspect.
1580
- copyPersistenceAnnotations( art, target ); // after chooseAnnotation()
1581
- return art;
613
+ dict[prop || id] = annotate;
614
+ return annotate;
1582
615
  }
1583
616
 
1584
- function addProxyElements( proxyDict, elements, inferred, location, prefix = '', anno = '' ) {
1585
- // TODO: also use for includeMembers()?
1586
- for (const name in elements) {
1587
- const pname = `${ prefix }${ name }`;
1588
- const origin = elements[name];
1589
- const proxy = linkToOrigin( origin, pname, null, null, location || origin.location );
1590
- proxy.$inferred = inferred;
1591
- if (origin.masked)
1592
- proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked );
1593
- if (origin.key)
1594
- proxy.key = Object.assign( { $inferred: 'include' }, origin.key );
1595
- if (anno)
1596
- setAnnotation( proxy, anno );
1597
- dictAdd( proxyDict.elements, pname, proxy );
617
+ function extendHandleReturns( extensions, art ) {
618
+ for (const ext of extensions || []) {
619
+ if (ext.$syntax === 'returns') { // TODO tmp: no proper XSN representation
620
+ ext.$syntax = '$inside-returns';
621
+ delete ext.params;
622
+ }
623
+ else {
624
+ warning( 'ext-expected-returns', [ ext.name.location, ext ], {
625
+ '#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',
626
+ }, {
627
+ std: 'Expected $(CODE)', // unused variant
628
+ action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)',
629
+ function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
630
+ } );
631
+ }
1598
632
  }
1599
633
  }
1600
634
 
635
+ // const unexpected_props = {
636
+ // elements: 'anno-unexpected-elements',
637
+ // enum: 'anno-unexpected-elements', // TODO
638
+ // params: 'anno-unexpected-params',
639
+ // actions: 'anno-unexpected-actions',
640
+ // };
641
+ // const undefined_props = {
642
+ // elements: 'anno-undefined-element',
643
+ // enum: 'anno-undefined-element', // TODO
644
+ // params: 'anno-undefined-param',
645
+ // actions: 'anno-undefined-action',
646
+ // };
1601
647
 
1602
- // Phase 4 - annotations ---------------------------------------------------
1603
- // move to top
1604
-
1605
- /**
1606
- * Goes through all (applied) annotations in the given artifact and chooses one
1607
- * if multiple exist according to the module layer.
1608
- * TODO: rename to extendArtifactBefore
1609
- *
1610
- * @param {XSN.Artifact} art
1611
- */
1612
- function chooseAnnotationsInArtifact( art ) {
1613
- // for main artifacts, move extensions from `$lateExtensions` model dictionary:
1614
- if (!art._main && !art._outer && art._extensions === undefined &&
1615
- art.kind !== 'namespace') {
1616
- // if (!art.name) console.log(art)
1617
- const { absolute } = art.name;
1618
- setLink( art, '_extensions', model.$lateExtensions[absolute]?._extensions || null );
1619
- if (art._extensions && !art.builtin) { // keep extensions for builtin in $lateExtensions
1620
- delete model.$lateExtensions[absolute];
1621
- // TODO: if the extension mechanism has been completed, we could uncomment:
1622
- // art._extensions.forEach( ext => resolvePath( ext.name, ext.kind, ext )); // for LSP
1623
- // for now, we do that at the end of lateExtensions()
648
+ function checkRemainingMemberExtensions( parent, ext, prop, name ) {
649
+ // console.log('CRME:',prop,name,parent,ext)
650
+ const dict = parent[prop];
651
+ if (!dict) {
652
+ // TODO: check - for each name? - better locations
653
+ const location = ext._parent[prop][$location] || ext.name.location;
654
+ // Remark: no `elements` dict location with `annotate Main:elem`
655
+ switch (prop) {
656
+ // TODO: change texts, somehow similar to checkDefinitions() ?
657
+ case 'elements':
658
+ case 'enum': // TODO: extra?
659
+ warning( 'anno-unexpected-elements', [ location, ext._parent ],
660
+ { '#': (parent._effectiveType?.kind === 'entity') ? 'entity' : 'std' }, {
661
+ std: 'Elements only exist in entities, types or typed constructs',
662
+ entity: 'Elements of entity types can\'t be annotated',
663
+ });
664
+ break;
665
+ case 'params':
666
+ warning( 'anno-unexpected-params', [ location, ext._parent ], {},
667
+ 'Parameters only exist for actions or functions' );
668
+ break;
669
+ case 'actions':
670
+ warning( 'anno-unexpected-actions', [ location, ext._parent ], {},
671
+ 'Actions and functions only exist top-level and for entities' );
672
+ break;
673
+ default:
674
+ // assert
1624
675
  }
676
+ return false;
1625
677
  }
1626
- if (art._extensions) {
1627
- // TODO: the following function can now be simplified
1628
- // if (art.$inferred) console.log('CAI:', art.name, art.$inferred,art._extensions)
1629
- // With extensions, member appears in CSN, affects directly the rendering of
1630
- // elements etc. TODO: do that more specifically on the dicts (via symbol)
1631
- // Probably better: we could use the _extensions dict prop directly in to-csn
1632
- if (art.$inferred)
1633
- setExpandStatusAnnotate( art, 'annotate' );
1634
- if (Array.isArray( art._extensions )) {
1635
- checkExtensionsKind( art._extensions, art ); // TODO: check with builtins
1636
- transformArtifactExtensions( art );
678
+ else if (!dict[name]) {
679
+ // TODO: make variant `returns` an auto-variant for ($ART) ?
680
+ const inReturns = parent._parent?.returns && parent._parent;
681
+ const art = inReturns || parent;
682
+ switch (prop) {
683
+ case 'elements':
684
+ notFound( 'anno-undefined-element', ext.name.location, ext,
685
+ { '#': (inReturns ? 'returns' : 'element'), art, name },
686
+ parent.elements );
687
+ break;
688
+ case 'enum': // TODO: extra msg id?
689
+ notFound( 'anno-undefined-element', ext.name.location, ext,
690
+ { '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
691
+ parent.enum );
692
+ break;
693
+ case 'params':
694
+ notFound( 'anno-undefined-param', ext.name.location, ext,
695
+ { '#': 'param', art: parent, name },
696
+ parent.params );
697
+ break;
698
+ case 'actions':
699
+ notFound( 'anno-undefined-action', ext.name.location, ext,
700
+ { '#': 'action', art: parent, name },
701
+ parent.actions );
702
+ break;
703
+ default:
704
+ // assert
1637
705
  }
1638
- applyAllExtensions( art );
1639
706
  }
707
+ return true;
1640
708
  }
1641
709
 
1642
- // TODO: if extensions has more than one of returns,items,elements,enum, delete all those props
1643
- function transformArtifactExtensions( art ) {
1644
- const hasOnlySubExtensions = art._outer; // items, anonymous aspects
1645
- const dict = Object.create(null);
1646
- for (const ext of art._extensions) {
1647
- for (const prop in ext) {
1648
- if (ext[prop] === undefined) // deleted propery
1649
- continue;
1650
- // TODO: do this check nicer (after complete move to new extensions mechanism)
1651
- if (prop.charAt(0) === '@' || prop === 'doc' ||
1652
- prop === 'includes' || prop === 'columns' ||
1653
- prop === 'length' || prop === 'scale' || prop === 'precision' || prop === 'srid') {
1654
- if (!hasOnlySubExtensions)
1655
- pushToDict( dict, prop, ext );
1656
- }
1657
- else if (prop === 'elements' || prop === 'enum' || prop === 'actions' ||
1658
- prop === 'params' || prop === 'returns') {
1659
- if (ext.kind === 'extend')
1660
- pushToDict( dict, 'includes', ext );
1661
- pushToDict( dict, prop, ext );
1662
- }
1663
- }
1664
- }
1665
- art._extensions = dict;
710
+ function notFound( msgId, location, address, args, validDict ) {
711
+ const msg = message( msgId, [ location, address ], args );
712
+ attachAndEmitValidNames( msg, validDict );
1666
713
  }
1667
714
 
1668
- function applyAllExtensions( art ) {
1669
- const extensions = art._extensions;
1670
- for (const prop in extensions) {
1671
- // TODO: do the following `if` in a nicer way
1672
- if ([ 'elements', 'enum', 'actions', 'params', 'returns' ].includes( prop ))
1673
- continue; // currently just annotates on sub elements - TODO: error here
1674
- // annotations, `doc`, `includes`, `columns`, `length`, ...
1675
- const scheduled = [];
1676
- // sort extensions according to layer (specified elements are bottom layer):
1677
- const layered = layeredExtensions( extensions[prop] );
715
+ // For createRemainingAnnotateStatements(): -----------------------------------
1678
716
 
1679
- let cont = true;
1680
- while (cont) {
1681
- const { highest, issue } = extensionsOfHighestLayers( layered );
1682
- // console.log( 'CA:', annoName, issue, extensions)
1683
- let index = highest.length;
1684
- cont = !!index; // safety
1685
- while (--index >= 0) {
1686
- const ext = highest[index];
1687
- scheduled.push( ext );
1688
- if (extensionOverwrites( ext, prop )) {
1689
- cont = false;
1690
- break;
1691
- }
717
+ function createSuperAnnotate( annotate ) {
718
+ const extensions = annotate._extensions;
719
+ if (extensions && !annotate._main) {
720
+ const { absolute } = annotate.name;
721
+ const isLocalized = absolute.startsWith( 'localized.' ); // TODO: && anno
722
+ const art = model.definitions[absolute];
723
+ for (const ext of extensions)
724
+ checkRemainingMainExtensions( art, ext, isLocalized );
725
+ if (art?.builtin && art.kind !== 'namespace') { // TODO: do not set `builtin` on cds, cds.hana
726
+ setLink( annotate, '_extensions', art._extensions ); // for messages and member extensions
727
+ // direct annotations on builtins or on the builtins for propagation, and
728
+ // also shallow-copied to $collectedExtensions for to-csn
729
+ for (const prop in art) {
730
+ if (prop.charAt(0) === '@' || prop === 'doc')
731
+ annotate[prop] = art[prop];
1692
732
  }
1693
- if (issue || index > 0)
1694
- reportDuplicateExtensions( highest, prop, issue, index, art );
1695
733
  }
1696
- // Now apply the relevant extensions
1697
- scheduled.reverse();
1698
- for (const ext of scheduled)
1699
- applySingleExtension( art, ext, prop );
1700
- delete extensions[prop];
734
+ if (extensions.length === 1) { // i.e. no proper location if from more than one extensions
735
+ annotate.location = extensions[0].location;
736
+ annotate.name.location = extensions[0].name.location;
737
+ }
1701
738
  }
739
+ extendArtifactBefore( annotate );
740
+ extendArtifactAfter( annotate );
741
+ forEachMember( annotate, createSuperAnnotate );
1702
742
  }
1703
743
 
1704
- function extensionOverwrites( ext, prop ) {
1705
- return (prop.charAt(0) !== '@')
1706
- ? [ 'doc', 'length', 'precision', 'scale', 'srid' ].includes( prop )
1707
- : !annotationHasEllipsis( ext[prop] );
744
+ function checkRemainingMainExtensions( art, ext, localized ) {
745
+ if (localized) // TODO v4: ignore only for annotate
746
+ return;
747
+ if (!resolvePath( ext.name, ext.kind, ext )) // error for extend, info for annotate
748
+ return;
749
+ // else if (ext.kind === 'extend') { // TODO v4 - add error
750
+ // }
751
+ if (art?.kind === 'namespace') {
752
+ // TODO: not at all different to having no definition
753
+ info( 'anno-namespace', [ ext.name.location, ext ], {}, // TODO: better location?
754
+ 'Namespaces can\'t be annotated' );
755
+ }
756
+ else if (art?.builtin) {
757
+ info( 'anno-builtin', [ ext.name.location, ext ], {}, // TODO: better location?
758
+ 'Builtin types should not be annotated. Use custom type instead' );
759
+ }
1708
760
  }
1709
761
 
1710
- // TODO: still a bit annotation assignment specific
1711
- function reportDuplicateExtensions( extensions, prop, issue, index, art ) {
1712
- // TODO: think about messages for these
1713
- if (prop === 'elements' || prop === 'enum' || prop === 'actions' || prop === 'columns' ||
1714
- prop === 'params' || prop === 'returns' || prop === 'includes' )
1715
- return; // extensions currently handled extra
1716
- if (issue) {
1717
- // eslint-disable-next-line no-nested-ternary
1718
- let msg = (index < 0)
1719
- ? 'anno-unstable-array'
1720
- : (issue === true)
1721
- ? 'anno-duplicate'
1722
- : 'anno-duplicate-unrelated-layer';
1723
- if (prop.charAt(0) !== '@' && prop !== 'doc') {
1724
- msg = (issue === true)
1725
- ? 'ext-duplicate-extend-type'
1726
- : 'ext-duplicate-extend-type-unrelated-layer';
1727
- // not sure whether to repeat the extended artifact in the message (we
1728
- // have the semantic location, after all)
762
+ // Issue messages for annotations on namespaces and builtins
763
+ // (TODO: really here?, probably split main artifacts vs returns)
764
+ // see also createRemainingAnnotateStatements() where similar messages are reported
765
+ function checkAnnotate( construct, art ) {
766
+ // TODO: Handle extend statements properly: Different message for empty extend?
767
+
768
+ // --> without art._block, art not found
769
+ if (construct.kind === 'annotate' && art._block?.$frontend === 'cdl') {
770
+ if (construct.$syntax === 'returns' && art.kind !== 'action' && art.kind !== 'function' ) {
771
+ // `annotate ABC with returns {}` is handled just like `elements`. Warn if it is used
772
+ // for non-actions. We can't only check for !art.returns, because `action A();` is valid.
773
+ // `art._block` ensures that `art` is a defined def.
774
+ return;
775
+ // warning('ext-unexpected-returns', [ construct.name.location, construct ],
776
+ // { keyword: 'returns', meta: art.kind }, 'Unexpected $(KEYWORD) for $(META)');
1729
777
  }
1730
- const variant = prop === 'doc' ? 'doc' : 'std';
1731
- for (const ext of extensions) {
1732
- const anno = ext[prop];
1733
- if (anno && !anno.$errorReported) {
1734
- message( msg, [ anno.name?.location || anno.location, ext ],
1735
- { '#': variant, anno: prop, type: art } );
1736
- }
778
+ else if (construct.$syntax !== 'returns' &&
779
+ (art.kind === 'action' || art.kind === 'function') && construct.elements) {
780
+ warning('ext-expected-returns', [ construct.name.location, construct ], {
781
+ '#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',
782
+ }, {
783
+ std: 'Expected $(CODE)', // unused variant
784
+ action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)',
785
+ function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
786
+ });
1737
787
  }
1738
788
  }
1739
- else if (index > 0) { // more than one set (not just ...)
1740
- const variant = prop === 'doc' ? 'doc' : 'std';
1741
- const msgid = (prop.charAt(0) === '@' || prop === 'doc')
1742
- ? 'anno-duplicate-same-file' // TODO: always ext-duplicate-…
1743
- : 'ext-duplicate-same-file';
1744
- while (index >= 0) { // do not report for trailing [...]
1745
- const ext = extensions[index--];
1746
- const anno = ext[prop];
1747
- warning( msgid, [ anno.name?.location || anno.location, ext ],
1748
- { '#': variant, prop, anno: prop } );
789
+ }
790
+
791
+ // extend, mainly old-style ---------------------------------------------------
792
+
793
+ /**
794
+ * Apply the extensions inside the extensionsDict on the model.
795
+ *
796
+ * First try normally: extends with structure includes; with remaining cyclic
797
+ * includes, do so without includes.
798
+ */
799
+ function applyExtensions() {
800
+ let noIncludes = false;
801
+ let extNames = Object.keys( extensionsDict ).sort();
802
+
803
+ while (extNames.length) {
804
+ const { length } = extNames;
805
+ for (const name of extNames) {
806
+ const art = model.definitions[name];
807
+ if (art && art.kind !== 'namespace' &&
808
+ extendArtifact( extensionsDict[name], art, noIncludes ))
809
+ delete extensionsDict[name];
1749
810
  }
811
+ extNames = Object.keys( extensionsDict ); // no sort() required anymore
812
+ if (extNames.length >= length)
813
+ noIncludes = Object.keys( extensionsDict ); // = no includes
1750
814
  }
1751
815
  }
1752
816
 
1753
- function applySingleExtension( art, ext, prop ) {
1754
- if (prop === 'includes') {
1755
- if (ext.kind === 'extend' && art.$inferred) {
1756
- error( 'extend-for-generated', [ ext.name.location, ext ], { art },
1757
- 'You can\'t use EXTEND on the generated $(ART)' );
1758
- }
1759
- else if (art.kind !== 'annotate' && !art._outer) { // not with elem extension in targetAspect
1760
- const { absolute } = art.name;
1761
- const dict = extensionsDict[absolute] || (extensionsDict[absolute] = []);
1762
- dict.push( ext ); // TODO: change
1763
- // console.log( 'ASI:',prop,art.name,ext,extensionsDict[absolute])
1764
- }
1765
- // art[prop] = (art[prop]) ? art[prop].concat( ext[prop] ) : ext[prop];
817
+ /**
818
+ * Extend artifact `art` by `extensions`. `noIncludes` can have values:
819
+ * - false: includes are applied, extend and annotate is performed
820
+ * - true: includes are not applied, extend and annotate is performed
821
+ * - 'gen': no includes and no extensions allowed, annotate is performed
822
+ *
823
+ * @param {XSN.Extension[]} extensions
824
+ * @param {XSN.Definition} art
825
+ * @param {boolean|'gen'} [noIncludes=false]
826
+ */
827
+ function extendArtifact( extensions, art, noIncludes = false ) {
828
+ if (!noIncludes && !(canApplyIncludes( art, art ) &&
829
+ extensions.every( ext => canApplyIncludes(ext, art) )))
830
+ return false;
831
+ if (Array.isArray( noIncludes )) {
832
+ canApplyIncludes( art, art, noIncludes );
833
+ extensions.forEach( ext => canApplyIncludes( ext, art, noIncludes ) );
1766
834
  }
1767
- else if (prop === 'columns') {
1768
- const { query } = art;
1769
- if (!query?.from?.path)
1770
- error( 'extend-columns', [ ext.columns[$location], ext ], { art } );
1771
- else if (!query.columns)
1772
- query.columns = [ { location: query.from.location, val: '*' }, ...ext.columns ];
1773
- else
1774
- query.columns.push( ...ext.columns );
835
+ else if (!noIncludes &&
836
+ !(canApplyIncludes( art, art ) &&
837
+ extensions.every( ext => canApplyIncludes( ext, art) ))) {
838
+ // console.log( 'FALSE:',art.name, extensions.map( e => e.name ) )
839
+ return false;
1775
840
  }
1776
- else if ([ 'length', 'precision', 'scale', 'srid' ].includes( prop )) {
1777
- const typeExts = art.$typeExts || (art.$typeExts = {});
1778
- typeExts[prop] = ext;
841
+ if (!art.query) {
842
+ model._entities.push( art ); // add structure with includes in dep order
843
+ art.$entity = ++model.$entity;
1779
844
  }
1780
- else {
1781
- const result = applyAssignment( art[prop], ext[prop], ext, prop );
1782
- art[prop] = (result.name) ? result : Object.assign( {}, art[prop], result );
845
+ if (!noIncludes && art.includes)
846
+ applyIncludes( art, art );
847
+ // checkExtensionsKind( extensions, art );
848
+ extendMembers( extensions, art, noIncludes === 'gen' );
849
+ if (!noIncludes && art.includes) {
850
+ // early propagation of specific annotation assignments
851
+ propagateEarly( art, '@cds.autoexpose' );
852
+ propagateEarly( art, '@fiori.draft.enabled' );
1783
853
  }
854
+ // TODO: complain about element extensions inside projection
855
+ return true;
1784
856
  }
1785
857
 
1786
- function applyAssignment( previousAnno, anno, art, annoName ) {
1787
- const firstEllipsis = annotationHasEllipsis( anno );
1788
- if (!firstEllipsis)
1789
- return anno;
1790
- const hasBase = previousAnno?.literal === 'array';
1791
- if (!previousAnno) {
1792
- const loc = firstEllipsis.location || anno.name.location;
1793
- message( 'anno-unexpected-ellipsis', [ loc, art ], { code: '...' } );
1794
- previousAnno = { val: [] };
858
+ function extendMembers( extensions, art, noExtend ) {
859
+ // TODO: do the whole extension stuff lazily if the elements are requested
860
+ const elemExtensions = [];
861
+ if (art._main) // extensions already sorted for main artifacts
862
+ extensions.sort( layers.compareLayer );
863
+ // TODO: use same sequence as in chooseAssignment() - better: use common code with that fn
864
+ // console.log('EM:',art.name,extensions,art._extensions)
865
+ for (const ext of extensions) { // those in extMap.includes
866
+ // console.log(message( 'id', [ext.location, ext], { art: ext.name._artifact },
867
+ // 'Info', 'EXT').toString())
868
+ if (ext.name._artifact === undefined) { // not already applied
869
+ setArtifactLink( ext.name, art );
870
+ if (noExtend && ext.kind === 'extend') {
871
+ error( 'extend-for-generated', [ ext.name.location, ext ], { art },
872
+ 'You can\'t use EXTEND on the generated $(ART)' );
873
+ continue;
874
+ }
875
+ if (ext.includes) {
876
+ // TODO: currently, re-compiling from gensrc does not give the exact
877
+ // element sequence - we need something like
878
+ // includes = ['Base1',3,'Base2']
879
+ // where 3 means adding the next 3 elements before applying include 'Base2'
880
+ if (art.includes)
881
+ art.includes.push(...ext.includes);
882
+ else
883
+ art.includes = [ ...ext.includes ];
884
+ applyIncludes( ext, art );
885
+ }
886
+ // console.log(ext,art)
887
+ checkAnnotate( ext, art );
888
+ // TODO: do we allow to add elements with array of {...}? If yes, adapt
889
+ initMembers( ext, art, ext._block ); // might set _extend, _annotate
890
+ dependsOnSilent(art, ext); // art depends silently on ext (inverse to normal dep!)
891
+ }
892
+ for (const name in ext.elements) {
893
+ const elem = ext.elements[name];
894
+ if (elem.kind === 'element') { // i.e. not extend or annotate
895
+ elemExtensions.push( elem );
896
+ break; // more than one elem in same EXTEND is fine
897
+ }
898
+ }
1795
899
  }
1796
- else if (previousAnno.literal !== 'array') {
1797
- // TODO: If we introduce sub-messages, point to the non-array base value.
1798
- error( 'anno-mismatched-ellipsis', [ anno.name.location, art ], { code: '...' } );
1799
- previousAnno = { val: [] };
900
+ if (elemExtensions.length > 1)
901
+ reportUnstableExtensions( elemExtensions );
902
+
903
+ // This whole function will be removed with a next change - no need to have nice code here:
904
+ const dict = Object.create(null);
905
+ // actions cannot be extended anyway. TODO: there should be a message
906
+ // (possible with CSN input), but that was missing before this change, too.
907
+ for (const e of extensions) {
908
+ if (!e.elements)
909
+ continue;
910
+ for (const n in e.elements) {
911
+ if (e.elements[n].kind === 'extend')
912
+ pushToDict( dict, n, e.elements[n] );
913
+ }
1800
914
  }
1801
- const previousValue = previousAnno.val;
1802
- let prevPos = 0;
1803
- const result = [];
1804
- for (const item of anno.val) {
1805
- const ell = item && item.literal === 'token' && item.val === '...';
1806
- if (!ell) {
1807
- result.push( item );
915
+ for (const name in dict) {
916
+ let obj = art;
917
+ if (obj.targetAspect)
918
+ obj = obj.targetAspect;
919
+ while (obj.items)
920
+ obj = obj.items;
921
+ const validDict = obj.elements || obj.enum;
922
+ const member = validDict && validDict[name];
923
+ if (!member)
924
+ extendNothing( dict[name], 'elements', name, art, validDict );
925
+ else if (!(member.$duplicates))
926
+ extendMembers( dict[name], member );
927
+ }
928
+ }
929
+
930
+ /**
931
+ * Report 'Warning: Unstable element order due to repeated extensions'
932
+ * except if all extensions are in the same file.
933
+ *
934
+ * @param {XSN.Extension[]} extensions
935
+ */
936
+ function reportUnstableExtensions( extensions ) {
937
+ // No message if all extensions are in the same file:
938
+ const file = layers.realname( extensions[0] );
939
+ if (extensions.every( ( ext, i ) => !i || file === layers.realname( ext ) ))
940
+ return;
941
+ // Similar to chooseAssignment(), TODO there: also extra intralayer message
942
+ // as this is a modeling error
943
+ let lastExt = null;
944
+ let open = []; // the "highest" layers
945
+ for (const ext of extensions) {
946
+ const extLayer = layers.layer( ext ) || { realname: '', _layerExtends: Object.create(null) };
947
+ if (!open.length) {
948
+ lastExt = ext;
949
+ open = [ extLayer.realname ];
1808
950
  }
1809
- else {
1810
- let upToSpec = item.upTo && checkUpToSpec( item.upTo, art, annoName, true );
1811
- while (prevPos < previousValue.length) {
1812
- const prevItem = previousValue[prevPos++];
1813
- result.push( prevItem );
1814
- if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) {
1815
- upToSpec = false;
1816
- break;
1817
- }
951
+ else if (extLayer.realname === open[open.length - 1]) { // in same layer
952
+ if (lastExt) {
953
+ message( 'extend-repeated-intralayer', [ lastExt.location, lastExt ] );
954
+ lastExt = null;
1818
955
  }
1819
- if (upToSpec && hasBase) {
1820
- // non-matched UP TO; if there is no base to apply to, there is already an error.
1821
- warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' },
1822
- 'The $(CODE) value does not match any item in the base annotation $(ANNO)' );
956
+ message( 'extend-repeated-intralayer', [ ext.location, ext ] );
957
+ }
958
+ else {
959
+ if (lastExt && (open.length > 1 || !extLayer._layerExtends[open[0]])) {
960
+ // report for lastExt if that is unrelated to other open exts or current ext
961
+ message( 'extend-unrelated-layer', [ lastExt.location, lastExt ], {},
962
+ 'Unstable element order due to other extension in unrelated layer' );
1823
963
  }
964
+ lastExt = ext;
965
+ open = open.filter( name => !extLayer._layerExtends[name] );
966
+ open.push( extLayer.realname );
1824
967
  }
1825
968
  }
1826
- // console.log('TP:',previousValue.map(se),anno.val.map(se),'->',result.map(se))
1827
- return { val: result, literal: 'array' };
1828
969
  }
1829
- // function se(a) { return a.upTo ? [a.val,a.upTo.val] : a.val ; }
1830
970
 
1831
- function checkUpToSpec( upToSpec, art, annoName, isFullUpTo ) {
1832
- const { literal } = upToSpec;
1833
- if (!isFullUpTo) { // inside struct of UP TO
1834
- if (literal !== 'struct' && literal !== 'array' )
1835
- return true;
1836
- }
1837
- else if (literal === 'struct') {
1838
- return Object.values( upToSpec.struct ).every( v => checkUpToSpec( v, art, annoName ) );
971
+
972
+ /**
973
+ * @param {XSN.Extension[]} extensions
974
+ * @param {string} prop
975
+ * @param {string} name
976
+ * @param {XSN.Artifact} art
977
+ * @param {object} validDict
978
+ */
979
+ function extendNothing( extensions, prop, name, art, validDict ) {
980
+ const artName = searchName( art, name, dictKinds[prop] );
981
+ for (const ext of extensions) {
982
+ // TODO: use shared functionality with notFound in resolver.js
983
+ const { location } = ext.name;
984
+ const extName = { ...artName, kind: ext.kind };
985
+ const msg
986
+ = error( 'extend-undefined', [ location, extName ],
987
+ { art: artName },
988
+ {
989
+ std: 'Unknown $(ART) - nothing to extend',
990
+ // eslint-disable-next-line max-len
991
+ element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend',
992
+ action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
993
+ } );
994
+ attachAndEmitValidNames(msg, validDict);
1839
995
  }
1840
- else if (literal !== 'array' && literal !== 'boolean' && literal !== 'null') {
996
+ }
997
+
998
+ // includes ----------------------------------------------------------------
999
+
1000
+ /**
1001
+ * Returns true, if `art.includes` can be applied on `target`.
1002
+ * They can't be applied if any of the artifacts referenced in
1003
+ * `art.includes` are yet to be extended.
1004
+ * `art !== target` if `art` is an extension.
1005
+ *
1006
+ * @param {XSN.Definition} art
1007
+ * @param {XSN.Artifact} target
1008
+ * @returns {boolean}
1009
+ */
1010
+ function canApplyIncludes( art, target, justResolveCyclic ) {
1011
+ if (!art.includes)
1841
1012
  return true;
1013
+ const isView = !!target.query;
1014
+ for (const ref of art.includes) {
1015
+ const name = resolveUncheckedPath( ref, 'include', art );
1016
+ // console.log('CAI:',justResolveCyclic, name, ref.path, Object.keys(extensionsDict))
1017
+ if (justResolveCyclic) {
1018
+ if (!justResolveCyclic.includes( name ))
1019
+ continue;
1020
+ delete ref._artifact;
1021
+ }
1022
+ else if (name && name in extensionsDict) {
1023
+ // one of the includes has itself extensions that need to be applied first
1024
+ return false;
1025
+ }
1026
+ else if (ref._artifact) {
1027
+ delete ref._artifact;
1028
+ }
1029
+ resolvePath( ref, isView ? 'viewInclude' : 'include', art );
1842
1030
  }
1843
- error( null, [ upToSpec.location, art ],
1844
- { anno: annoName, code: '... up to', '#': literal },
1845
- {
1846
- std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)',
1847
- array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)',
1848
- // eslint-disable-next-line max-len
1849
- struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)',
1850
- boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)',
1851
- null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)',
1852
- } );
1853
- return false;
1031
+ return true;
1854
1032
  }
1855
1033
 
1856
- function equalUpTo( previousItem, upToSpec ) {
1857
- if (!previousItem)
1858
- return false;
1859
- if ('val' in upToSpec) {
1860
- if (previousItem.val === upToSpec.val) // enum, struct and ref have no val
1861
- return true;
1862
- const typeUpTo = typeof upToSpec.val;
1863
- const typePrev = typeof previousItem.val;
1864
- if (typeUpTo === 'number')
1865
- return typePrev === 'string' && previousItem.val === upToSpec.val.toString();
1866
- if (typePrev === 'number')
1867
- return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString();
1868
- }
1869
- else if (upToSpec.path) {
1870
- return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec );
1871
- }
1872
- else if (upToSpec.sym) {
1873
- return previousItem.sym && previousItem.sym.id === upToSpec.sym.id;
1034
+ /**
1035
+ * Apply all includes of `ext` on `ext`. Checks that `art` allows includes.
1036
+ * If `ext === art`, then includes of the artifact itself are applied.
1037
+ * If `ext !== art`, applies includes on the extensions, not artifact.
1038
+ * Sets `_ancestor` links on `art`.
1039
+ *
1040
+ * Examples:
1041
+ * ext === art: `entity E : F {}` => add elements of F to E
1042
+ * ext !== art: `extend E with F` => add elements of F to extension on E
1043
+ *
1044
+ * @param {XSN.Extension} ext
1045
+ * @param {XSN.Artifact} art
1046
+ */
1047
+ function applyIncludes( ext, art ) {
1048
+ if (kindProperties[art.kind].include !== true) {
1049
+ error( 'extend-unexpected-include', [ ext.includes[0]?.location, ext ],
1050
+ { meta: art.kind } );
1051
+ return;
1874
1052
  }
1875
- else if (upToSpec.struct && previousItem.struct) {
1876
- return Object.entries( upToSpec.struct )
1877
- .every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) );
1053
+
1054
+ if (!art.query) {
1055
+ if (!art._ancestors)
1056
+ setLink( art, '_ancestors', [] ); // recursive array of includes
1057
+ for (const ref of ext.includes) {
1058
+ const template = ref._artifact;
1059
+ // !template -> non-includable, e.g. scalar type, or cyclic
1060
+ if (template) {
1061
+ if (template._ancestors)
1062
+ art._ancestors.push( ...template._ancestors );
1063
+ art._ancestors.push( template );
1064
+ }
1065
+ }
1878
1066
  }
1879
- return false;
1067
+ if (!art.query) // do not set art.elements and art.enums with query entity!
1068
+ includeMembers( ext, art, 'elements' );
1069
+ includeMembers( ext, art, 'actions' );
1880
1070
  }
1881
1071
 
1882
- function normalizeRef( node ) { // see to-csn.js
1883
- const ref = pathName( node.path );
1884
- return node.variant ? `${ ref }#${ node.variant.id }` : ref;
1072
+ /**
1073
+ * Add all members (e.g. elements or actions) of `ext.includes` to `ext[prop]`.
1074
+ * If `art` is `ext`, set the parent link accordingly.
1075
+ *
1076
+ * @param {XSN.Extension} ext
1077
+ * @param {XSN.Artifact} art
1078
+ * @param {string} prop: 'elements' or 'actions'
1079
+ */
1080
+ function includeMembers( ext, art, prop ) {
1081
+ // TODO two kind of messages:
1082
+ // Error 'More than one include defines element "A"' (at include ref)
1083
+ // Warning 'Overwrites definition from include "I" (at elem def)
1084
+ const parent = ext === art && art;
1085
+ const members = ext[prop];
1086
+ ext[prop] = Object.create(null); // TODO: do not set actions property if there are none
1087
+ for (const ref of ext.includes) {
1088
+ const template = ref._artifact; // already resolved
1089
+ if (template) { // be robust
1090
+ forEachInOrder( template, prop, ( origin, name ) => {
1091
+ if (members && name in members)
1092
+ return; // warning for overwritten element in checks.js
1093
+ const elem = linkToOrigin( origin, name, parent, prop, weakLocation( ref.location ) );
1094
+ if (!parent) // not yet set for EXTEND foo WITH bar => linkToOrigin() did not add it
1095
+ dictAdd( ext[prop], name, elem );
1096
+ elem.$inferred = 'include';
1097
+ if (origin.masked)
1098
+ elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );
1099
+ if (origin.key)
1100
+ elem.key = Object.assign( { $inferred: 'include' }, origin.key );
1101
+ if (origin.value && origin.$syntax === 'calc') {
1102
+ // TODO: If paths become invalid in the new artifact, should we mark
1103
+ // all usages in the expressions? Possibly just the first one?
1104
+ elem.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
1105
+ elem.$syntax = 'calc';
1106
+ setLink( elem, '_calcOrigin', origin._calcOrigin || origin );
1107
+ }
1108
+ // TODO: also complain if elem is just defined in art
1109
+ });
1110
+ }
1111
+ }
1112
+ checkRedefinitionThroughIncludes( parent, prop );
1113
+ // TODO: expand elements having direct elements (if needed)
1114
+ if (members) {
1115
+ forEachInOrder( { [prop]: members }, prop, ( elem, name ) => {
1116
+ dictAdd( ext[prop], name, elem );
1117
+ });
1118
+ }
1885
1119
  }
1886
1120
 
1887
1121
  /**
1888
- * Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from
1889
- * source to target if present on source but not target.
1122
+ * Report duplicates in parent[prop] that happen due to multiple includes having the
1123
+ * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
1890
1124
  *
1891
- * @param {object} target
1892
- * @param {object} source
1125
+ * TODO(v4): Make this a hard error; see checkRedefinition(); maybe combine both;
1893
1126
  */
1894
- function copyPersistenceAnnotations( target, source ) {
1895
- if (!source)
1127
+ function checkRedefinitionThroughIncludes( parent, prop ) {
1128
+ if (!parent[prop])
1896
1129
  return;
1897
-
1898
- const copyExists = !isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' );
1899
- if (copyExists)
1900
- copy( '@cds.persistence.exists' );
1901
- copy( '@cds.persistence.skip' );
1902
-
1903
- function copy( anno ) {
1904
- if ( source[anno] && !target[anno] )
1905
- target[anno] = { ...source[anno], $inferred: 'parent-origin' };
1906
- }
1130
+ forEachInOrder(parent, prop, ( member, name ) => {
1131
+ if (member.$inferred === 'include' && Array.isArray(member.$duplicates)) {
1132
+ const includes = [ member, ...member.$duplicates ].map(dup => dup._origin._main);
1133
+ if (isBetaEnabled(options, 'v4preview')) {
1134
+ error( 'duplicate-definition', [ parent.name.location, member ],
1135
+ { '#': `include-${ prop }`, name, sorted_arts: includes } );
1136
+ }
1137
+ else {
1138
+ // Error accidentally removed in v2/v3, therefore only a warning.
1139
+ warning( 'ref-duplicate-include-member', [ parent.name.location, member ],
1140
+ { '#': prop, name, sorted_arts: includes } );
1141
+ }
1142
+ }
1143
+ });
1907
1144
  }
1908
1145
  }
1909
1146
 
@@ -1917,7 +1154,8 @@ function extend( model ) {
1917
1154
  function layeredExtensions( extensions ) {
1918
1155
  const layered = Object.create(null);
1919
1156
  for (const ext of extensions) {
1920
- const layer = (ext.kind === 'annotate' || ext.kind === 'extend') && layers.layer( ext );
1157
+ const layer = (ext.kind === 'annotate' || ext.kind === 'extend' || ext.kind === 'source') &&
1158
+ layers.layer( ext );
1921
1159
  // just consider layer if Extend/Annotate, not Define
1922
1160
  const name = (layer) ? layer.realname : '';
1923
1161
  const done = layered[name];
@@ -1990,72 +1228,33 @@ function inMoreThanOneFile( extensions ) {
1990
1228
  * Returns <0 if `a`<`b`, >1 if `a`>`b`, i.e. can be used for ascending sort.
1991
1229
  */
1992
1230
  function compareExtensions( a, b ) {
1993
- const fileA = layers.realname( a._block );
1994
- const fileB = layers.realname( b._block );
1231
+ const fileA = layers.realname( a );
1232
+ const fileB = layers.realname( b );
1995
1233
  if (fileA !== fileB)
1996
1234
  return (fileA > fileB) ? 1 : -1;
1997
1235
  return (a?.location?.line || 0) - (b?.location?.line || 0) ||
1998
1236
  (a?.location?.col || 0) - (b?.location?.col || 0);
1999
1237
  }
2000
1238
 
2001
- function augmentEqual( location, assocname, relations, prefix = '' ) {
2002
- const args = relations.map( eq );
2003
- return (args.length === 1)
2004
- ? args[0]
2005
- : { op: { val: 'and', location }, args, location };
2006
-
2007
- function eq( refs ) {
2008
- if (Array.isArray(refs))
2009
- return { op: { val: '=', location }, args: refs.map( ref ), location };
2010
-
2011
- const { id } = refs.name;
2012
- return {
2013
- op: { val: '=', location },
2014
- args: [
2015
- { path: [ { id: assocname, location }, { id, location } ], location },
2016
- { path: [ { id: `${ prefix }${ id }`, location } ], location },
2017
- ],
2018
- location,
2019
- };
2020
- }
2021
- function ref( path ) {
2022
- return { path: path.split('.').map( id => ({ id, location }) ), location };
2023
- }
2024
- }
2025
1239
 
2026
1240
  /**
2027
- * If the given extension is a `EXTEND <def> WITH TYPE` extension, store
2028
- * it in the given artifact. resolve.js will resolve types and call
2029
- * `typeExtensions()` later.
1241
+ * Propagate the given `prop` (e.g. annotation) early, i.e. copy it from all `.includes`
1242
+ * if they have the property.
2030
1243
  *
2031
- * @param {XSN.Extension} ext
2032
- * @param {object} art
1244
+ * @param {XSN.Definition} art
1245
+ * @param {string} prop
2033
1246
  */
2034
- // function storeTypeExtension( ext, art ) {
2035
- // // If there are no parameters to apply, don't store the extension.
2036
- // if (!typeParameters.list.some( prop => ext[prop] !== undefined ))
2037
- // return;
2038
- // else if (!art._extendType)
2039
- // setLink( art, '_extendType', [] );
2040
- // art._extendType.push( ext );
2041
- // }
2042
-
2043
- function checkTextsLanguageAssocOption( model, options ) {
2044
- const languages = model.definitions['sap.common.Languages'];
2045
- const commonLanguagesEntity = options.addTextsLanguageAssoc && languages?.elements?.code;
2046
-
2047
- if (options.addTextsLanguageAssoc && !commonLanguagesEntity) {
2048
- const variant = !languages ? 'std' : 'code';
2049
- const loc = model.definitions['sap.common.Languages']?.name?.location || null;
2050
- model.$messageFunctions.info('api-ignoring-language-assoc', loc, {
2051
- '#': variant, option: 'addTextsLanguageAssoc', art: 'sap.common.Languages', name: 'code',
2052
- }, {
2053
- std: 'Ignoring option $(OPTION) because entity $(ART) is missing',
2054
- code: 'Ignoring option $(OPTION) because entity $(ART) is missing element $(NAME)',
2055
- });
1247
+ function propagateEarly( art, prop ) {
1248
+ if (art[prop])
1249
+ return;
1250
+ for (const ref of art.includes) {
1251
+ const aspect = ref._artifact;
1252
+ if (aspect) {
1253
+ const anno = aspect[prop];
1254
+ if (anno && (anno.val !== null || !art[prop]))
1255
+ art[prop] = Object.assign( { $inferred: 'include' }, anno );
1256
+ }
2056
1257
  }
2057
-
2058
- return !!commonLanguagesEntity;
2059
1258
  }
2060
1259
 
2061
1260