@sap/cds-compiler 3.8.0 → 3.9.2

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