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