@sap/cds-compiler 3.6.2 → 3.8.0
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 +109 -1
- package/README.md +3 -0
- package/bin/cdsc.js +12 -5
- package/doc/CHANGELOG_ARCHIVE.md +6 -6
- package/doc/CHANGELOG_BETA.md +35 -2
- package/doc/CHANGELOG_DEPRECATED.md +2 -2
- package/doc/DeprecatedOptions_v2.md +1 -1
- package/doc/NameResolution.md +1 -1
- package/lib/api/main.js +63 -23
- package/lib/api/options.js +1 -0
- package/lib/api/validate.js +5 -0
- package/lib/base/dictionaries.js +15 -3
- package/lib/base/keywords.js +2 -0
- package/lib/base/message-registry.js +120 -34
- package/lib/base/messages.js +51 -27
- package/lib/base/model.js +4 -2
- package/lib/base/shuffle.js +2 -1
- package/lib/checks/arrayOfs.js +1 -1
- package/lib/checks/defaultValues.js +1 -1
- package/lib/checks/elements.js +29 -1
- package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +10 -6
- package/lib/checks/invalidTarget.js +1 -1
- package/lib/checks/nonexpandableStructured.js +1 -1
- package/lib/checks/onConditions.js +15 -9
- package/lib/checks/sql-snippets.js +2 -2
- package/lib/checks/types.js +5 -1
- package/lib/checks/validator.js +7 -3
- package/lib/compiler/assert-consistency.js +42 -26
- package/lib/compiler/base.js +50 -4
- package/lib/compiler/builtins.js +17 -8
- package/lib/compiler/checks.js +241 -246
- package/lib/compiler/define.js +113 -146
- package/lib/compiler/extend.js +889 -383
- package/lib/compiler/finalize-parse-cdl.js +5 -58
- package/lib/compiler/index.js +1 -1
- package/lib/compiler/kick-start.js +7 -8
- package/lib/compiler/populate.js +297 -293
- package/lib/compiler/propagator.js +27 -18
- package/lib/compiler/resolve.js +146 -463
- package/lib/compiler/shared.js +36 -79
- package/lib/compiler/tweak-assocs.js +30 -28
- package/lib/compiler/utils.js +31 -5
- package/lib/edm/annotations/genericTranslation.js +131 -59
- package/lib/edm/annotations/preprocessAnnotations.js +3 -0
- package/lib/edm/csn2edm.js +22 -5
- package/lib/edm/edm.js +6 -4
- package/lib/edm/edmAnnoPreprocessor.js +1 -0
- package/lib/edm/edmPreprocessor.js +42 -26
- package/lib/gen/Dictionary.json +38 -2
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +3 -1
- package/lib/gen/languageLexer.js +1 -1
- package/lib/gen/languageParser.js +4828 -4472
- package/lib/inspect/inspectPropagation.js +20 -34
- package/lib/json/from-csn.js +140 -44
- package/lib/json/to-csn.js +114 -122
- package/lib/language/errorStrategy.js +2 -0
- package/lib/language/genericAntlrParser.js +156 -36
- package/lib/language/language.g4 +100 -58
- package/lib/language/textUtils.js +13 -0
- package/lib/main.d.ts +43 -3
- package/lib/main.js +4 -2
- package/lib/model/csnRefs.js +15 -3
- package/lib/model/csnUtils.js +12 -74
- package/lib/model/revealInternalProperties.js +4 -2
- package/lib/modelCompare/compare.js +2 -1
- package/lib/optionProcessor.js +3 -0
- package/lib/render/manageConstraints.js +5 -2
- package/lib/render/toCdl.js +216 -104
- package/lib/render/toHdbcds.js +2 -9
- package/lib/render/toRename.js +14 -51
- package/lib/render/toSql.js +4 -3
- package/lib/render/utils/common.js +9 -5
- package/lib/transform/braceExpression.js +6 -0
- package/lib/transform/db/assertUnique.js +2 -1
- package/lib/transform/db/expansion.js +2 -0
- package/lib/transform/db/flattening.js +37 -36
- package/lib/transform/db/rewriteCalculatedElements.js +600 -0
- package/lib/transform/db/transformExists.js +4 -0
- package/lib/transform/db/views.js +40 -37
- package/lib/transform/forOdataNew.js +20 -15
- package/lib/transform/forRelationalDB.js +58 -41
- package/lib/transform/odata/typesExposure.js +50 -15
- package/lib/transform/parseExpr.js +16 -8
- package/lib/transform/transformUtilsNew.js +42 -14
- package/lib/transform/translateAssocsToJoins.js +60 -37
- package/lib/transform/universalCsn/coreComputed.js +15 -7
- package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
- package/package.json +2 -1
package/lib/compiler/extend.js
CHANGED
|
@@ -9,21 +9,31 @@ const { searchName, weakLocation } = require('../base/messages');
|
|
|
9
9
|
const {
|
|
10
10
|
isDeprecatedEnabled,
|
|
11
11
|
forEachGeneric, forEachInOrder, forEachDefinition,
|
|
12
|
+
forEachMember,
|
|
13
|
+
isBetaEnabled,
|
|
12
14
|
} = require('../base/model');
|
|
13
|
-
const { dictAdd } = require('../base/dictionaries');
|
|
15
|
+
const { dictAdd, pushToDict } = require('../base/dictionaries');
|
|
14
16
|
const { kindProperties, dictKinds } = require('./base');
|
|
15
17
|
const {
|
|
16
18
|
setLink,
|
|
17
19
|
setArtifactLink,
|
|
18
|
-
|
|
20
|
+
copyExpr,
|
|
21
|
+
setAnnotation,
|
|
22
|
+
setExpandStatusAnnotate,
|
|
19
23
|
linkToOrigin,
|
|
20
24
|
setMemberParent,
|
|
21
25
|
dependsOnSilent,
|
|
22
26
|
augmentPath,
|
|
23
|
-
|
|
27
|
+
pathName,
|
|
28
|
+
splitIntoPath,
|
|
29
|
+
isDirectComposition,
|
|
30
|
+
annotationHasEllipsis,
|
|
24
31
|
} = require('./utils');
|
|
25
32
|
const layers = require('./moduleLayers');
|
|
26
|
-
|
|
33
|
+
|
|
34
|
+
const $location = Symbol.for('cds.$location');
|
|
35
|
+
|
|
36
|
+
const genLocation = { file: '' }; // attach stupid location - TODO: remove in v4
|
|
27
37
|
|
|
28
38
|
function extend( model ) {
|
|
29
39
|
const { options } = model;
|
|
@@ -34,23 +44,22 @@ function extend( model ) {
|
|
|
34
44
|
const {
|
|
35
45
|
resolvePath,
|
|
36
46
|
resolveUncheckedPath,
|
|
37
|
-
|
|
38
|
-
initAnnotations,
|
|
39
|
-
copyAnnotationsForExtensions,
|
|
47
|
+
resolveTypeArgumentsUnchecked,
|
|
40
48
|
attachAndEmitValidNames,
|
|
41
|
-
checkDefinitions,
|
|
42
49
|
initArtifact,
|
|
43
50
|
initMembers,
|
|
44
|
-
extensionsDict, // not a function - TODO
|
|
45
51
|
} = model.$functions;
|
|
46
52
|
|
|
47
53
|
Object.assign( model.$functions, {
|
|
48
54
|
lateExtensions,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
applyTypeExtensions,
|
|
55
|
+
chooseAnnotationsInArtifact,
|
|
56
|
+
extendArtifactAfter,
|
|
52
57
|
} );
|
|
53
58
|
|
|
59
|
+
const extensionsDict = Object.create(null);
|
|
60
|
+
forEachDefinition( model, tagIncludes ); // TODO TMP
|
|
61
|
+
|
|
62
|
+
forEachDefinition( model, chooseAnnotationsInArtifact );
|
|
54
63
|
applyExtensions();
|
|
55
64
|
|
|
56
65
|
const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
|
|
@@ -58,9 +67,8 @@ function extend( model ) {
|
|
|
58
67
|
|
|
59
68
|
Object.keys( model.definitions ).forEach( processArtifact );
|
|
60
69
|
|
|
61
|
-
lateExtensions( false );
|
|
62
|
-
|
|
63
70
|
compositionChildPersistence();
|
|
71
|
+
return;
|
|
64
72
|
|
|
65
73
|
/**
|
|
66
74
|
* Process "composition of" artifacts.
|
|
@@ -110,90 +118,442 @@ function extend( model ) {
|
|
|
110
118
|
if (def.$inferred === 'composition-entity' && !processed.has(def)) {
|
|
111
119
|
if (def._parent)
|
|
112
120
|
processCompositionPersistence(def._parent);
|
|
113
|
-
copyPersistenceAnnotations(def, def._parent
|
|
121
|
+
copyPersistenceAnnotations( def, def._parent );
|
|
114
122
|
processed.add(def);
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
125
|
}
|
|
118
126
|
|
|
127
|
+
// TMP:
|
|
128
|
+
function tagIncludes( art ) {
|
|
129
|
+
if (art.includes)
|
|
130
|
+
extensionsDict[art.name.absolute] = [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//-----------------------------------------------------------------------------
|
|
134
|
+
// Extensions: general algorithm
|
|
135
|
+
//-----------------------------------------------------------------------------
|
|
136
|
+
// extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements
|
|
137
|
+
|
|
138
|
+
// TODO: assert that we have not yet transformed/used _extensions on sub elements
|
|
139
|
+
// TODO necessary(?): transformArtifactExtensions must ensure that each annotate
|
|
140
|
+
// is in either returns,items,elements,enum
|
|
141
|
+
function extendArtifactAfter( art ) {
|
|
142
|
+
const extensionsMap = art._extensions;
|
|
143
|
+
if (!extensionsMap || art.builtin) // builtin members handled via "super annotate"
|
|
144
|
+
return;
|
|
145
|
+
// type extensions after having “populated” the artifact ($typeArgs -> length,
|
|
146
|
+
// …, TODO: do that there) and setting an _effectiveType:
|
|
147
|
+
if (art.$typeExts) {
|
|
148
|
+
const { type } = art; // if the type is not inferred, it is the origin...
|
|
149
|
+
if (type?._artifact && !type.$inferred) // ...and thus is resolved
|
|
150
|
+
resolveTypeArgumentsUnchecked( art, type._artifact, art );
|
|
151
|
+
const exts = art.$typeExts;
|
|
152
|
+
applyTypeExtensions( art, exts.length, 'length' );
|
|
153
|
+
const scaleDiff = applyTypeExtensions( art, exts.scale, 'scale' );
|
|
154
|
+
applyTypeExtensions( art, exts.precision, 'precision', scaleDiff );
|
|
155
|
+
applyTypeExtensions( art, exts.srid, 'srid' );
|
|
156
|
+
delete art.$typeExts;
|
|
157
|
+
}
|
|
158
|
+
// TODO tmp: no proper XSN representation yet for annotate … with returns:
|
|
159
|
+
if (art.kind === 'annotate' && !art.returns &&
|
|
160
|
+
(extensionsMap.elements?.some( e => e.$syntax === 'returns' ) ||
|
|
161
|
+
extensionsMap.enum?.some( e => e.$syntax === 'returns' )))
|
|
162
|
+
annotateCreate( art, '', art, 'returns' );
|
|
163
|
+
|
|
164
|
+
moveDictExtensions( art, extensionsMap, 'params' );
|
|
165
|
+
// moveReturnsExtensions( art, extensionsMap );
|
|
166
|
+
const sub = art.returns || art.items || art.targetAspect?.elements && art.targetAspect;
|
|
167
|
+
if (sub) {
|
|
168
|
+
if (art.returns) { // after having applied params!
|
|
169
|
+
extendHandleReturns( extensionsMap.elements, art );
|
|
170
|
+
extendHandleReturns( extensionsMap.enum, art );
|
|
171
|
+
}
|
|
172
|
+
// care about 'ext-unexpected-returns' in a later change if we have XSN returns
|
|
173
|
+
pushToDict( sub, '_extensions', ...extensionsMap.elements || [] );
|
|
174
|
+
pushToDict( sub, '_extensions', ...extensionsMap.enum || [] );
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
moveDictExtensions( art, extensionsMap,
|
|
178
|
+
(art.enum && art.kind !== 'annotate' ? 'enum' : 'elements'), 'elements' );
|
|
179
|
+
moveDictExtensions( art, extensionsMap, 'enum' );
|
|
180
|
+
}
|
|
181
|
+
moveDictExtensions( art, extensionsMap, 'actions' );
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create super annotate statements for remaining extensions
|
|
186
|
+
*/
|
|
187
|
+
function lateExtensions() { // -> createRemainingAnnotateStatements
|
|
188
|
+
model.extensions = Object.values( model.$lateExtensions );
|
|
189
|
+
// TODO: testMode sort?
|
|
190
|
+
model.extensions.forEach( createSuperAnnotate );
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// For extendArtifactAfter(): -------------------------------------------------
|
|
194
|
+
|
|
195
|
+
// Remarks on messages: we allow the type extensions only if the artifact
|
|
196
|
+
// originally had that property → any check of the kind “type prop can only be
|
|
197
|
+
// used with FooBar” is independent from `extend … with type`. Function
|
|
198
|
+
// checkTypeArguments() in resolve.js reports 'type-unexpected-argument', but
|
|
199
|
+
// that is currently incomplete.
|
|
200
|
+
//
|
|
201
|
+
// We then report (in the future), use the first message of:
|
|
202
|
+
// - the usual messages if a type argument is wrong, independently from `extend`
|
|
203
|
+
// - 'ext-unexpected-type-argument' (TODO) if the artifact does not have the prop
|
|
204
|
+
// - 'ext-invalid-type-argument' if the value is wrong for extend (no overwrite)
|
|
205
|
+
//
|
|
206
|
+
// TODO v4: do not allow `extend … with (precision: …)` alone if original def also has `scale`
|
|
207
|
+
function applyTypeExtensions( art, ext, prop, scaleDiff ) {
|
|
208
|
+
// console.log('ATE:',art?.[prop],ext?.[prop],scaleDiff)
|
|
209
|
+
if (!ext?.[prop])
|
|
210
|
+
return 0;
|
|
211
|
+
if (!art[prop]) {
|
|
212
|
+
const isBuiltin = art._effectiveType?.builtin;
|
|
213
|
+
if (isBuiltin && !allowsTypeArgument( art, prop )) {
|
|
214
|
+
// Let checkTypeArguments() in resolve.js report a message, is incomplete
|
|
215
|
+
// though, i.e. can only safely be used for scalars at the moment. But we
|
|
216
|
+
// will improve that function and not try to do extra things here.
|
|
217
|
+
art[prop] = ext[prop]; // enable checkTypeArguments() doing its job
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
// TODO: think about 'ext-unexpected-type-argument'
|
|
221
|
+
error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
|
|
222
|
+
{ '#': (isBuiltin ? 'indirect' : 'new-prop'), prop } );
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
const artVal = art[prop].val;
|
|
226
|
+
const extVal = ext[prop].val;
|
|
227
|
+
if (prop === 'srid') {
|
|
228
|
+
error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'prop', prop } );
|
|
229
|
+
}
|
|
230
|
+
else if (typeof artVal !== 'number' || typeof extVal !== 'number' ) {
|
|
231
|
+
// Users can't change from/to string value for property,
|
|
232
|
+
// e.g. `variable`/`floating` for Decimal
|
|
233
|
+
// TODO: Shouldn't the text distinguish between orig string and extension string?
|
|
234
|
+
// Not sure whether to talk about strings if we have a keyword in CDL
|
|
235
|
+
error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'string', prop } );
|
|
236
|
+
}
|
|
237
|
+
else if (extVal < artVal + (scaleDiff || 0)) {
|
|
238
|
+
const number = artVal + (scaleDiff || 0);
|
|
239
|
+
error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
|
|
240
|
+
// eslint-disable-next-line object-curly-newline
|
|
241
|
+
{ '#': (scaleDiff ? 'scale' : 'number'), prop, number, otherprop: 'scale' } );
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
art[prop] = ext[prop];
|
|
245
|
+
return extVal - artVal;
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function allowsTypeArgument( art, prop ) {
|
|
251
|
+
const { parameters } = art._effectiveType;
|
|
252
|
+
if (!parameters)
|
|
253
|
+
return false;
|
|
254
|
+
return parameters.includes( prop ) || parameters[0]?.name === prop;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function moveDictExtensions( art, extensionsMap, artProp, extProp = artProp ) {
|
|
258
|
+
// TODO: setExpandStatusAnnotate
|
|
259
|
+
const extensions = extensionsMap[extProp];
|
|
260
|
+
if (!extensions)
|
|
261
|
+
return;
|
|
262
|
+
const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate
|
|
263
|
+
|
|
264
|
+
for (const ext of extensions) {
|
|
265
|
+
const extDict = ext[extProp];
|
|
266
|
+
for (const name in extDict) {
|
|
267
|
+
let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement
|
|
268
|
+
const elemExt = extDict[name];
|
|
269
|
+
if (elemExt.kind !== 'annotate' && elemExt.kind !== 'extend') // TODO: specified elems
|
|
270
|
+
continue; // definitions inside extend, already handled
|
|
271
|
+
dictCheck = dictCheck && checkRemainingMemberExtensions( art, elemExt, artProp, name );
|
|
272
|
+
const elem = artDict[name] || annotateFor( art, extProp, name );
|
|
273
|
+
setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null) );
|
|
274
|
+
pushToDict( elem, '_extensions', elemExt );
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// function moveReturnsExtensions( art, extensionsMap ) {
|
|
280
|
+
// const artReturns = art.returns;
|
|
281
|
+
// const extensions = extensionsMap.returns;
|
|
282
|
+
// // TODO: artItem is null
|
|
283
|
+
// for (const ext of extensions)
|
|
284
|
+
// pushToDict( artReturns, '_extensions', ext.returns );
|
|
285
|
+
// }
|
|
286
|
+
|
|
287
|
+
function annotateFor( art, prop, name ) {
|
|
288
|
+
const base = annotateBase( art );
|
|
289
|
+
if (name === '' && prop === 'params')
|
|
290
|
+
return base.returns || annotateCreate( base, name, base, 'returns' );
|
|
291
|
+
const dict = base[prop] || (base[prop] = Object.create( null ));
|
|
292
|
+
if (name == null)
|
|
293
|
+
return dict;
|
|
294
|
+
return dict[name] || annotateCreate( dict, name, base );
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function annotateBase( art ) {
|
|
298
|
+
while (art._outer) // TOOD: think about anonymous target aspect
|
|
299
|
+
art = art._outer;
|
|
300
|
+
// if (art._annotateS)
|
|
301
|
+
// return art._annotateS;
|
|
302
|
+
if (art.kind === 'annotate')
|
|
303
|
+
return art;
|
|
304
|
+
|
|
305
|
+
// TODO: more to do if annotate can have `returns` property
|
|
306
|
+
if (art.kind === 'select')
|
|
307
|
+
art = art._parent;
|
|
308
|
+
if (art._main)
|
|
309
|
+
return annotateFor( art._parent, kindProperties[art.kind].dict, art.name.id );
|
|
310
|
+
|
|
311
|
+
const { absolute } = art.name;
|
|
312
|
+
return model.$lateExtensions[absolute] || annotateCreate( model.$lateExtensions, absolute );
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function annotateCreate( dict, id, parent, prop ) {
|
|
316
|
+
const annotate = {
|
|
317
|
+
kind: 'annotate',
|
|
318
|
+
name: { id, location: genLocation },
|
|
319
|
+
$inferred: '',
|
|
320
|
+
location: genLocation,
|
|
321
|
+
};
|
|
322
|
+
if (parent) {
|
|
323
|
+
setLink( annotate, '_parent', parent );
|
|
324
|
+
setLink( annotate, '_main', parent._main || parent );
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
annotate.name.absolute = id; // TODO later (if all names are sparse): delete absolute
|
|
328
|
+
}
|
|
329
|
+
dict[prop || id] = annotate;
|
|
330
|
+
return annotate;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function extendHandleReturns( extensions, art ) {
|
|
334
|
+
for (const ext of extensions || []) {
|
|
335
|
+
if (ext.$syntax === 'returns') { // TODO tmp: no proper XSN representation
|
|
336
|
+
ext.$syntax = '$inside-returns';
|
|
337
|
+
delete ext.params;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
warning( 'ext-expected-returns', [ ext.name.location, ext ], {
|
|
341
|
+
'#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',
|
|
342
|
+
}, {
|
|
343
|
+
std: 'Expected $(CODE)', // unused variant
|
|
344
|
+
action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)',
|
|
345
|
+
function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
|
|
346
|
+
} );
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// const unexpected_props = {
|
|
352
|
+
// elements: 'anno-unexpected-elements',
|
|
353
|
+
// enum: 'anno-unexpected-elements', // TODO
|
|
354
|
+
// params: 'anno-unexpected-params',
|
|
355
|
+
// actions: 'anno-unexpected-actions',
|
|
356
|
+
// };
|
|
357
|
+
// const undefined_props = {
|
|
358
|
+
// elements: 'anno-undefined-element',
|
|
359
|
+
// enum: 'anno-undefined-element', // TODO
|
|
360
|
+
// params: 'anno-undefined-param',
|
|
361
|
+
// actions: 'anno-undefined-action',
|
|
362
|
+
// };
|
|
363
|
+
|
|
364
|
+
function checkRemainingMemberExtensions( parent, ext, prop, name ) {
|
|
365
|
+
// console.log('CRME:',prop,name,parent,ext)
|
|
366
|
+
const dict = parent[prop];
|
|
367
|
+
if (!dict) {
|
|
368
|
+
// TODO: check - for each name? - better locations
|
|
369
|
+
const location = ext._parent[prop][$location] || ext.name.location;
|
|
370
|
+
// Remark: no `elements` dict location with `annotate Main:elem`
|
|
371
|
+
switch (prop) {
|
|
372
|
+
// TODO: change texts, somehow similar to checkDefinitions() ?
|
|
373
|
+
case 'elements':
|
|
374
|
+
case 'enum': // TODO: extra?
|
|
375
|
+
warning( 'anno-unexpected-elements', [ location, ext._parent ],
|
|
376
|
+
{ '#': (parent._effectiveType?.kind === 'entity') ? 'entity' : 'std' }, {
|
|
377
|
+
std: 'Elements only exist in entities, types or typed constructs',
|
|
378
|
+
entity: 'Elements of entity types can\'t be annotated',
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
case 'params':
|
|
382
|
+
warning( 'anno-unexpected-params', [ location, ext._parent ], {},
|
|
383
|
+
'Parameters only exist for actions or functions' );
|
|
384
|
+
break;
|
|
385
|
+
case 'actions':
|
|
386
|
+
warning( 'anno-unexpected-actions', [ location, ext._parent ], {},
|
|
387
|
+
'Actions and functions only exist top-level and for entities' );
|
|
388
|
+
break;
|
|
389
|
+
default:
|
|
390
|
+
// assert
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
else if (!dict[name]) {
|
|
395
|
+
// TODO: make variant `returns` an auto-variant for ($ART) ?
|
|
396
|
+
const inReturns = parent._parent?.returns && parent._parent;
|
|
397
|
+
const art = inReturns || parent;
|
|
398
|
+
switch (prop) {
|
|
399
|
+
case 'elements':
|
|
400
|
+
notFound( 'anno-undefined-element', ext.name.location, ext,
|
|
401
|
+
{ '#': (inReturns ? 'returns' : 'element'), art, name },
|
|
402
|
+
parent.elements );
|
|
403
|
+
break;
|
|
404
|
+
case 'enum': // TODO: extra msg id?
|
|
405
|
+
notFound( 'anno-undefined-element', ext.name.location, ext,
|
|
406
|
+
{ '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
|
|
407
|
+
parent.enum );
|
|
408
|
+
break;
|
|
409
|
+
case 'params':
|
|
410
|
+
notFound( 'anno-undefined-param', ext.name.location, ext,
|
|
411
|
+
{ '#': 'param', art: parent, name },
|
|
412
|
+
parent.params );
|
|
413
|
+
break;
|
|
414
|
+
case 'actions':
|
|
415
|
+
notFound( 'anno-undefined-action', ext.name.location, ext,
|
|
416
|
+
{ '#': 'action', art: parent, name },
|
|
417
|
+
parent.actions );
|
|
418
|
+
break;
|
|
419
|
+
default:
|
|
420
|
+
// assert
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function notFound( msgId, location, address, args, validDict ) {
|
|
427
|
+
const msg = message( msgId, [ location, address ], args );
|
|
428
|
+
attachAndEmitValidNames( msg, validDict );
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// For createRemainingAnnotateStatements(): -----------------------------------
|
|
432
|
+
|
|
433
|
+
function createSuperAnnotate( annotate ) {
|
|
434
|
+
const extensions = annotate._extensions;
|
|
435
|
+
if (extensions && !annotate._main) {
|
|
436
|
+
const { absolute } = annotate.name;
|
|
437
|
+
const isLocalized = absolute.startsWith( 'localized.' ); // TODO: && anno
|
|
438
|
+
const art = model.definitions[absolute];
|
|
439
|
+
for (const ext of extensions)
|
|
440
|
+
checkRemainingMainExtensions( art, ext, isLocalized );
|
|
441
|
+
if (art?.builtin && art.kind !== 'namespace') { // TODO: do not set `builtin` on cds, cds.hana
|
|
442
|
+
setLink( annotate, '_extensions', art._extensions ); // for messages and member extensions
|
|
443
|
+
// direct annotations on builtins or on the builtins for propagation, and
|
|
444
|
+
// also shallow-copied to $collectedExtensions for to-csn
|
|
445
|
+
for (const prop in art) {
|
|
446
|
+
if (prop.charAt(0) === '@' || prop === 'doc')
|
|
447
|
+
annotate[prop] = art[prop];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (extensions.length === 1) { // i.e. no proper location if from more than one extensions
|
|
451
|
+
annotate.location = extensions[0].location;
|
|
452
|
+
annotate.name.location = extensions[0].name.location;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
chooseAnnotationsInArtifact( annotate );
|
|
456
|
+
extendArtifactAfter( annotate );
|
|
457
|
+
forEachMember( annotate, createSuperAnnotate );
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function checkRemainingMainExtensions( art, ext, localized ) {
|
|
461
|
+
if (localized) // TODO v4: ignore only for annotate
|
|
462
|
+
return;
|
|
463
|
+
if (!resolvePath( ext.name, ext.kind, ext )) // error for extend, info for annotate
|
|
464
|
+
return;
|
|
465
|
+
// else if (ext.kind === 'extend') { // TODO v4 - add error
|
|
466
|
+
// }
|
|
467
|
+
if (art?.kind === 'namespace') {
|
|
468
|
+
// TODO: not at all different to having no definition
|
|
469
|
+
info( 'anno-namespace', [ ext.name.location, ext ], {}, // TODO: better location?
|
|
470
|
+
'Namespaces can\'t be annotated' );
|
|
471
|
+
}
|
|
472
|
+
else if (art?.builtin) {
|
|
473
|
+
info( 'anno-builtin', [ ext.name.location, ext ], {}, // TODO: better location?
|
|
474
|
+
'Builtin types should not be annotated. Use custom type instead' );
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Issue messages for annotations on namespaces and builtins
|
|
479
|
+
// (TODO: really here?, probably split main artifacts vs returns)
|
|
480
|
+
// see also lateExtensions() where similar messages are reported
|
|
481
|
+
function checkAnnotate( construct, art ) {
|
|
482
|
+
// TODO: Handle extend statements properly: Different message for empty extend?
|
|
483
|
+
|
|
484
|
+
// --> without art._block, art not found
|
|
485
|
+
if (construct.kind === 'annotate' && art._block?.$frontend === 'cdl') {
|
|
486
|
+
if (construct.$syntax === 'returns' && art.kind !== 'action' && art.kind !== 'function' ) {
|
|
487
|
+
// `annotate ABC with returns {}` is handled just like `elements`. Warn if it is used
|
|
488
|
+
// for non-actions. We can't only check for !art.returns, because `action A();` is valid.
|
|
489
|
+
// `art._block` ensures that `art` is a defined def.
|
|
490
|
+
return;
|
|
491
|
+
// warning('ext-unexpected-returns', [ construct.name.location, construct ],
|
|
492
|
+
// { keyword: 'returns', meta: art.kind }, 'Unexpected $(KEYWORD) for $(META)');
|
|
493
|
+
}
|
|
494
|
+
else if (construct.$syntax !== 'returns' &&
|
|
495
|
+
(art.kind === 'action' || art.kind === 'function') && construct.elements) {
|
|
496
|
+
warning('ext-expected-returns', [ construct.name.location, construct ], {
|
|
497
|
+
'#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',
|
|
498
|
+
}, {
|
|
499
|
+
std: 'Expected $(CODE)', // unused variant
|
|
500
|
+
action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)',
|
|
501
|
+
function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
119
507
|
// extend ------------------------------------------------------------------
|
|
120
508
|
|
|
121
509
|
/**
|
|
122
510
|
* Apply the extensions inside the extensionsDict on the model.
|
|
123
511
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* Before phase 1: all artifact extensions have been collected (even those
|
|
128
|
-
* inside extend context), only "empty" ones from structure includes are still unknown.
|
|
129
|
-
* After phase 1, all main artifacts are known, also "empty" extensions are known.
|
|
512
|
+
* First try normally: extends with structure includes; with remaining cyclic
|
|
513
|
+
* includes, do so without includes.
|
|
130
514
|
*/
|
|
131
515
|
function applyExtensions() {
|
|
132
|
-
let
|
|
516
|
+
let noIncludes = false;
|
|
133
517
|
let extNames = Object.keys( extensionsDict ).sort();
|
|
134
|
-
|
|
135
|
-
// after the extend for C has been applied (which could have defined C.E).
|
|
136
|
-
// Looping over model.definitions in Phase 1 would miss the `extend
|
|
137
|
-
// context` for a context C.C defined in an `extend context C`.
|
|
138
|
-
//
|
|
139
|
-
// TODO: no need to sort anymore
|
|
518
|
+
|
|
140
519
|
while (extNames.length) {
|
|
141
520
|
const { length } = extNames;
|
|
142
521
|
for (const name of extNames) {
|
|
143
522
|
const art = model.definitions[name];
|
|
144
|
-
if (
|
|
145
|
-
|
|
523
|
+
if (art && art.kind !== 'namespace' &&
|
|
524
|
+
extendArtifact( extensionsDict[name], art, noIncludes ))
|
|
146
525
|
delete extensionsDict[name];
|
|
147
|
-
}
|
|
148
|
-
else if (art.$duplicates) { // cannot extend redefinitions
|
|
149
|
-
delete extensionsDict[name];
|
|
150
|
-
}
|
|
151
|
-
else if (phase === 1
|
|
152
|
-
? extendContext( name, art )
|
|
153
|
-
: extendArtifact( extensionsDict[name], art, phase > 2 )) { // >2: no self-include
|
|
154
|
-
delete extensionsDict[name];
|
|
155
|
-
}
|
|
156
526
|
}
|
|
157
527
|
extNames = Object.keys( extensionsDict ); // no sort() required anymore
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
else if (extNames.length >= length)
|
|
161
|
-
phase = 3;
|
|
528
|
+
if (extNames.length >= length)
|
|
529
|
+
noIncludes = Object.keys( extensionsDict ); // = no includes
|
|
162
530
|
}
|
|
163
531
|
}
|
|
164
532
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
533
|
+
function checkExtensionsKind( extensions, art ) {
|
|
534
|
+
for (const ext of extensions) {
|
|
535
|
+
const kind = ext.expectedKind?.val;
|
|
536
|
+
if (kind && kind !== art.kind) {
|
|
537
|
+
const loc = ext.expectedKind.location;
|
|
538
|
+
if (kind === 'context' || kind === 'service') {
|
|
539
|
+
// We have no real artifact during the construction of a super-annotate statement:
|
|
540
|
+
const msgArgs = {
|
|
541
|
+
'#': (art.kind === 'service' || art.kind === 'annotate') ? art.kind : 'std',
|
|
542
|
+
art,
|
|
543
|
+
kind,
|
|
544
|
+
code: 'extend … with definitions',
|
|
545
|
+
keyword: 'extend service',
|
|
546
|
+
};
|
|
547
|
+
warning( 'ext-invalid-kind', [ loc, ext ], msgArgs, {
|
|
548
|
+
std: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) instead',
|
|
549
|
+
annotate: 'There is no artifact $(ART), use $(CODE) instead',
|
|
550
|
+
// do not mention 'extend context', that is not in CAPire
|
|
551
|
+
service: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) or $(KEYWORD) instead',
|
|
178
552
|
});
|
|
179
553
|
}
|
|
554
|
+
// TODO: Use similar checks for EXTEND ENTITY etc - 'ext-ignoring-kind'
|
|
180
555
|
}
|
|
181
|
-
return false;
|
|
182
556
|
}
|
|
183
|
-
|
|
184
|
-
for (const ext of extensionsDict[name]) {
|
|
185
|
-
setArtifactLink( ext.name, art );
|
|
186
|
-
checkDefinitions( ext, art, 'elements'); // error for elements etc
|
|
187
|
-
checkDefinitions( ext, art, 'enum');
|
|
188
|
-
checkDefinitions( ext, art, 'actions');
|
|
189
|
-
checkDefinitions( ext, art, 'params');
|
|
190
|
-
checkDefinitions( ext, art, 'columns');
|
|
191
|
-
if (ext.includes)
|
|
192
|
-
applyIncludes( ext, art ); // emits error if `includes` is set
|
|
193
|
-
checkAnnotate( ext, art );
|
|
194
|
-
copyAnnotationsForExtensions( ext, art );
|
|
195
|
-
}
|
|
196
|
-
return true;
|
|
197
557
|
}
|
|
198
558
|
|
|
199
559
|
/**
|
|
@@ -210,12 +570,23 @@ function extend( model ) {
|
|
|
210
570
|
if (!noIncludes && !(canApplyIncludes( art, art ) &&
|
|
211
571
|
extensions.every( ext => canApplyIncludes(ext, art) )))
|
|
212
572
|
return false;
|
|
573
|
+
if (Array.isArray( noIncludes )) {
|
|
574
|
+
canApplyIncludes( art, art, noIncludes );
|
|
575
|
+
extensions.forEach( ext => canApplyIncludes( ext, art, noIncludes ) );
|
|
576
|
+
}
|
|
577
|
+
else if (!noIncludes &&
|
|
578
|
+
!(canApplyIncludes( art, art ) &&
|
|
579
|
+
extensions.every( ext => canApplyIncludes( ext, art) ))) {
|
|
580
|
+
// console.log( 'FALSE:',art.name, extensions.map( e => e.name ) )
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
213
583
|
if (!art.query) {
|
|
214
584
|
model._entities.push( art ); // add structure with includes in dep order
|
|
215
585
|
art.$entity = ++model.$entity;
|
|
216
586
|
}
|
|
217
587
|
if (!noIncludes && art.includes)
|
|
218
588
|
applyIncludes( art, art );
|
|
589
|
+
// checkExtensionsKind( extensions, art );
|
|
219
590
|
extendMembers( extensions, art, noIncludes === 'gen' );
|
|
220
591
|
if (!noIncludes && art.includes) {
|
|
221
592
|
// early propagation of specific annotation assignments
|
|
@@ -229,12 +600,14 @@ function extend( model ) {
|
|
|
229
600
|
function extendMembers( extensions, art, noExtend ) {
|
|
230
601
|
// TODO: do the whole extension stuff lazily if the elements are requested
|
|
231
602
|
const elemExtensions = [];
|
|
232
|
-
|
|
603
|
+
if (art._main) // extensions already sorted for main artifacts
|
|
604
|
+
extensions.sort( layers.compareLayer );
|
|
233
605
|
// TODO: use same sequence as in chooseAssignment() - better: use common code with that fn
|
|
234
|
-
|
|
606
|
+
// console.log('EM:',art.name,extensions,art._extensions)
|
|
607
|
+
for (const ext of extensions) { // those in extMap.includes
|
|
235
608
|
// console.log(message( 'id', [ext.location, ext], { art: ext.name._artifact },
|
|
236
609
|
// 'Info', 'EXT').toString())
|
|
237
|
-
if (
|
|
610
|
+
if (ext.name._artifact === undefined) { // not already applied
|
|
238
611
|
setArtifactLink( ext.name, art );
|
|
239
612
|
if (noExtend && ext.kind === 'extend') {
|
|
240
613
|
error( 'extend-for-generated', [ ext.name.location, ext ], { art },
|
|
@@ -252,12 +625,10 @@ function extend( model ) {
|
|
|
252
625
|
art.includes = [ ...ext.includes ];
|
|
253
626
|
applyIncludes( ext, art );
|
|
254
627
|
}
|
|
628
|
+
// console.log(ext,art)
|
|
255
629
|
checkAnnotate( ext, art );
|
|
256
|
-
initAnnotations( ext, ext._block, ext.kind ); // TODO: do in define.js
|
|
257
|
-
copyAnnotationsForExtensions( ext, art );
|
|
258
630
|
// TODO: do we allow to add elements with array of {...}? If yes, adapt
|
|
259
631
|
initMembers( ext, art, ext._block ); // might set _extend, _annotate
|
|
260
|
-
storeTypeExtension( ext, art );
|
|
261
632
|
dependsOnSilent(art, ext); // art depends silently on ext (inverse to normal dep!)
|
|
262
633
|
}
|
|
263
634
|
for (const name in ext.elements) {
|
|
@@ -267,17 +638,24 @@ function extend( model ) {
|
|
|
267
638
|
break; // more than one elem in same EXTEND is fine
|
|
268
639
|
}
|
|
269
640
|
}
|
|
270
|
-
|
|
271
|
-
if (ext.columns) // extend projection
|
|
272
|
-
extendColumns( ext, art );
|
|
273
641
|
}
|
|
274
642
|
if (elemExtensions.length > 1)
|
|
275
643
|
reportUnstableExtensions( elemExtensions );
|
|
276
|
-
if (art._extendType && art._extendType.length > 0)
|
|
277
|
-
reportTypeExtensionsInSameLayer( art._extendType );
|
|
278
644
|
|
|
645
|
+
// This whole function will be removed with a next change - no need to have nice code here:
|
|
646
|
+
const extsTmp = { elements: Object.create(null), actions: Object.create(null) };
|
|
647
|
+
for (const e of extensions) {
|
|
648
|
+
for (const n in e.elements || []) {
|
|
649
|
+
if (e.elements[n].kind === 'extend')
|
|
650
|
+
pushToDict( extsTmp.elements, n, e.elements[n] );
|
|
651
|
+
}
|
|
652
|
+
for (const n in extensions.actions || []) {
|
|
653
|
+
if (e.actions[n].kind === 'extend')
|
|
654
|
+
pushToDict( extsTmp.actions, n, e.actions[n] );
|
|
655
|
+
}
|
|
656
|
+
}
|
|
279
657
|
[ 'elements', 'actions' ].forEach( (prop) => {
|
|
280
|
-
const dict =
|
|
658
|
+
const dict = extsTmp[prop];
|
|
281
659
|
for (const name in dict) {
|
|
282
660
|
let obj = art;
|
|
283
661
|
if (obj.targetAspect)
|
|
@@ -371,129 +749,6 @@ function extend( model ) {
|
|
|
371
749
|
return !hasError;
|
|
372
750
|
}
|
|
373
751
|
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Copy columns for EXTEND PROJECTION
|
|
377
|
-
*
|
|
378
|
-
* @param {XSN.Extension} ext
|
|
379
|
-
* @param {XSN.Artifact} art
|
|
380
|
-
*/
|
|
381
|
-
function extendColumns( ext, art ) {
|
|
382
|
-
// TODO: consider reportUnstableExtensions
|
|
383
|
-
|
|
384
|
-
const { location } = ext.name;
|
|
385
|
-
const { query } = art;
|
|
386
|
-
if (!query) {
|
|
387
|
-
if (art.kind !== 'annotate')
|
|
388
|
-
error( 'extend-columns', [ location, ext ], { art } );
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (!query.from || !query.from.path) {
|
|
392
|
-
error( 'extend-columns', [ location, ext ], { art } );
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
if (!query.columns)
|
|
396
|
-
query.columns = [ { location, val: '*' } ];
|
|
397
|
-
|
|
398
|
-
for (const column of ext.columns) {
|
|
399
|
-
setLink( column, '_block', ext._block );
|
|
400
|
-
query.columns.push(column);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Similar to chooseAssignment for annotations, this function applies type extensions in order,
|
|
407
|
-
* such that hierarchies are respected.
|
|
408
|
-
* Order already set in `extendMembers()` using `compareLayers()`.
|
|
409
|
-
*
|
|
410
|
-
* @param art
|
|
411
|
-
*/
|
|
412
|
-
function applyTypeExtensions( art ) {
|
|
413
|
-
/**
|
|
414
|
-
* Contains the previous extension for each property that was applied
|
|
415
|
-
* successfully.
|
|
416
|
-
*/
|
|
417
|
-
const previousSuccess = Object.create(null);
|
|
418
|
-
const allowedBuiltinCategories = [ 'string', 'decimal', 'integer', 'binary' ];
|
|
419
|
-
const artType = art.type?._artifact;
|
|
420
|
-
|
|
421
|
-
for (const ext of art._extendType) {
|
|
422
|
-
if (art.$inferred) {
|
|
423
|
-
// Only report first extension for $inferred artifact. Reduces noise.
|
|
424
|
-
error('ref-expected-scalar-type', [ ext.name.location, ext ], { '#': 'inferred' } );
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const baseType = art._effectiveType; // may be an ENUM
|
|
429
|
-
if (!artType || (art.kind !== 'type' && art.kind !== 'element') || !baseType?.builtin) {
|
|
430
|
-
// Only report first extension for non-scalar base type. Reduces noise.
|
|
431
|
-
error('ref-expected-scalar-type', [ ext.name.location, ext ], { } );
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
else if (!allowedBuiltinCategories.includes(baseType.category)) {
|
|
435
|
-
// Only report first extension for non-scalar type. Reduces noise.
|
|
436
|
-
error('ref-expected-scalar-type', [ ext.name.location, ext ],
|
|
437
|
-
{ '#': 'unsupported', prop: baseType.category });
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
for (const prop of typeParameters.list) {
|
|
442
|
-
if (!ext[prop])
|
|
443
|
-
continue;
|
|
444
|
-
|
|
445
|
-
if (!baseType.parameters?.includes(prop)) {
|
|
446
|
-
// For `extend T with type (length:10)` where T does not expect a length.
|
|
447
|
-
error( 'type-unexpected-argument', [ ext[prop].location, ext ], {
|
|
448
|
-
'#': 'type', prop, art, type: baseType,
|
|
449
|
-
});
|
|
450
|
-
break; // one error for first property is enough
|
|
451
|
-
}
|
|
452
|
-
else if (!art[prop]) {
|
|
453
|
-
// Can only extend properties that exist directly on the artifact.
|
|
454
|
-
error('ext-invalid-type-property', [ ext[prop].location, ext ],
|
|
455
|
-
{ prop, '#': 'new-prop' });
|
|
456
|
-
break; // one error for first property is enough
|
|
457
|
-
}
|
|
458
|
-
else if (art[prop].val === ext[prop].val) {
|
|
459
|
-
// Ignore extensions with same value
|
|
460
|
-
}
|
|
461
|
-
else if (art[prop].literal === 'string' || ext[prop].literal === 'string' ) {
|
|
462
|
-
// Users can't change from/to string value for property,
|
|
463
|
-
// e.g. `variable`/`floating` for Decimal
|
|
464
|
-
error( 'ext-invalid-type-property', [ ext[prop].location, ext ],
|
|
465
|
-
{ '#': 'string', prop } );
|
|
466
|
-
}
|
|
467
|
-
else if (art[prop].val > ext[prop].val) {
|
|
468
|
-
// TODO: Future: Sub-message that points to previous extension?
|
|
469
|
-
error( 'ext-invalid-type-property', [ ext[prop].location, ext ], {
|
|
470
|
-
prop,
|
|
471
|
-
value: ext[prop].val,
|
|
472
|
-
number: art[prop].val,
|
|
473
|
-
'#': previousSuccess[prop] ? 'ext-smaller' : 'smaller',
|
|
474
|
-
} );
|
|
475
|
-
break; // one error for first property is enough
|
|
476
|
-
}
|
|
477
|
-
else if (art[prop].val < ext[prop].val) {
|
|
478
|
-
art[prop] = ext[prop];
|
|
479
|
-
previousSuccess[prop] = ext;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// If `scale` is increased, but `precision` is not, data may be lost in a migration.
|
|
484
|
-
// e.g. `Decimal(4,2)` allows `12.34`. Increasing scale to 3 would make this value
|
|
485
|
-
// invalid and only allow `1.234`.
|
|
486
|
-
// TODO: Should we actually check that the increase is correct?
|
|
487
|
-
if (ext.scale && !ext.precision) {
|
|
488
|
-
error('ext-invalid-type-property', [ ext.scale.location, ext ], {
|
|
489
|
-
prop: 'scale',
|
|
490
|
-
otherprop: 'precision',
|
|
491
|
-
'#': 'scale',
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
752
|
/**
|
|
498
753
|
* Report 'Warning: Unstable element order due to repeated extensions'
|
|
499
754
|
* except if all extensions are in the same file.
|
|
@@ -535,26 +790,6 @@ function extend( model ) {
|
|
|
535
790
|
}
|
|
536
791
|
}
|
|
537
792
|
|
|
538
|
-
/**
|
|
539
|
-
* Report type extensions in same layer, similar mechanism to chooseAssignment()
|
|
540
|
-
* for annotations.
|
|
541
|
-
*
|
|
542
|
-
* @param {XSN.Extension[]} extensions
|
|
543
|
-
*/
|
|
544
|
-
function reportTypeExtensionsInSameLayer( extensions ) {
|
|
545
|
-
// Group assignments by layer
|
|
546
|
-
const extLayers = layeredAssignments( extensions );
|
|
547
|
-
// We only care about the highest layer
|
|
548
|
-
const { assignments, issue } = assignmentsOfHighestLayers( extLayers );
|
|
549
|
-
|
|
550
|
-
if (issue) {
|
|
551
|
-
const id = (issue === 'unrelated')
|
|
552
|
-
? 'ext-duplicate-extend-type-unrelated-layer'
|
|
553
|
-
: 'ext-duplicate-extend-type';
|
|
554
|
-
for (const ext of assignments)
|
|
555
|
-
message( id, [ ext.name.location, ext ], { type: ext.name._artifact } );
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
793
|
|
|
559
794
|
/**
|
|
560
795
|
* @param {XSN.Extension[]} extensions
|
|
@@ -582,77 +817,6 @@ function extend( model ) {
|
|
|
582
817
|
}
|
|
583
818
|
}
|
|
584
819
|
|
|
585
|
-
/**
|
|
586
|
-
* @param {Function|false} [veryLate]
|
|
587
|
-
*/
|
|
588
|
-
function lateExtensions( veryLate ) {
|
|
589
|
-
for (const name in model.$lateExtensions) {
|
|
590
|
-
const art = model.definitions[name];
|
|
591
|
-
const exts = model.$lateExtensions[name];
|
|
592
|
-
if (art && art.kind !== 'namespace') {
|
|
593
|
-
if (art.builtin) {
|
|
594
|
-
for (const ext of exts)
|
|
595
|
-
info( 'anno-builtin', [ ext.name.location, ext ] );
|
|
596
|
-
}
|
|
597
|
-
// created texts entity, auto-exposed entity
|
|
598
|
-
if (exts) {
|
|
599
|
-
extendArtifact( exts, art, 'gen' );
|
|
600
|
-
if (veryLate)
|
|
601
|
-
veryLate( art );
|
|
602
|
-
model.$lateExtensions[name] = null; // done
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
else if (veryLate) {
|
|
606
|
-
// Complain about unused extensions, i.e. those
|
|
607
|
-
// which do not point to a valid artifact
|
|
608
|
-
for (const ext of exts) {
|
|
609
|
-
delete ext.name.path[0]._artifact; // get message for root
|
|
610
|
-
// TODO: make resolvePath('extend'/'annotate') ignore namespaces
|
|
611
|
-
// Don't try to apply annotations in the `localized.` namespace.
|
|
612
|
-
// That's done in `localized.js`.
|
|
613
|
-
if (!name.startsWith('localized.') &&
|
|
614
|
-
resolvePath( ext.name, ext.kind, ext )) { // should issue error/info
|
|
615
|
-
// should issue error for cds extensions (annotate ok)
|
|
616
|
-
if (art.kind === 'namespace') {
|
|
617
|
-
// TODO: Emit error if namespace is extended by non-definitions.
|
|
618
|
-
info( 'anno-namespace', [ ext.name.location, ext ], {},
|
|
619
|
-
'Namespaces can\'t be annotated' );
|
|
620
|
-
}
|
|
621
|
-
// Builtin annotations would be represented as annotations in to-csn.js
|
|
622
|
-
else if (art.builtin) {
|
|
623
|
-
info( 'anno-builtin', [ ext.name.location, ext ] );
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
// TODO: warning for context/service extension on non-correct
|
|
627
|
-
if (ext.kind === 'annotate')
|
|
628
|
-
delete ext.name._artifact; // make it be considered by extendArtifact()
|
|
629
|
-
}
|
|
630
|
-
// create "super" ANNOTATE containing all non-applied ones
|
|
631
|
-
const first = exts[0];
|
|
632
|
-
const { location } = first.name;
|
|
633
|
-
|
|
634
|
-
/** @type {XSN.Definition} */
|
|
635
|
-
const annotationArtifact = {
|
|
636
|
-
kind: 'annotate',
|
|
637
|
-
name: { path: [ { id: name, location } ], absolute: name, location },
|
|
638
|
-
location: first.location,
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
if (!model.extensions)
|
|
642
|
-
model.extensions = [];
|
|
643
|
-
|
|
644
|
-
model.extensions.push(annotationArtifact);
|
|
645
|
-
extendArtifact( exts, annotationArtifact ); // also sets _artifact link in extensions
|
|
646
|
-
// if one of the annotate statement mentions 'returns', assume it
|
|
647
|
-
// TODO: with warning/info?
|
|
648
|
-
for (const ext of exts) {
|
|
649
|
-
if (ext.$syntax === 'returns')
|
|
650
|
-
annotationArtifact.$syntax = 'returns';
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
820
|
// includes ----------------------------------------------------------------
|
|
657
821
|
|
|
658
822
|
/**
|
|
@@ -665,14 +829,26 @@ function extend( model ) {
|
|
|
665
829
|
* @param {XSN.Artifact} target
|
|
666
830
|
* @returns {boolean}
|
|
667
831
|
*/
|
|
668
|
-
function canApplyIncludes( art, target ) {
|
|
669
|
-
if (art.includes)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
832
|
+
function canApplyIncludes( art, target, justResolveCyclic ) {
|
|
833
|
+
if (!art.includes)
|
|
834
|
+
return true;
|
|
835
|
+
const isView = !!target.query;
|
|
836
|
+
for (const ref of art.includes) {
|
|
837
|
+
const name = resolveUncheckedPath( ref, 'include', art );
|
|
838
|
+
// console.log('CAI:',justResolveCyclic, name, ref.path, Object.keys(extensionsDict))
|
|
839
|
+
if (justResolveCyclic) {
|
|
840
|
+
if (!justResolveCyclic.includes( name ))
|
|
841
|
+
continue;
|
|
842
|
+
delete ref._artifact;
|
|
675
843
|
}
|
|
844
|
+
else if (name && name in extensionsDict) {
|
|
845
|
+
// one of the includes has itself extensions that need to be applied first
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
else if (ref._artifact) {
|
|
849
|
+
delete ref._artifact;
|
|
850
|
+
}
|
|
851
|
+
resolvePath( ref, isView ? 'viewInclude' : 'include', art );
|
|
676
852
|
}
|
|
677
853
|
return true;
|
|
678
854
|
}
|
|
@@ -710,7 +886,8 @@ function extend( model ) {
|
|
|
710
886
|
}
|
|
711
887
|
}
|
|
712
888
|
}
|
|
713
|
-
|
|
889
|
+
if (!art.query) // do not set art.elements and art.enums with query entity!
|
|
890
|
+
includeMembers( ext, art, 'elements' );
|
|
714
891
|
includeMembers( ext, art, 'actions' );
|
|
715
892
|
}
|
|
716
893
|
|
|
@@ -743,10 +920,18 @@ function extend( model ) {
|
|
|
743
920
|
elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );
|
|
744
921
|
if (origin.key)
|
|
745
922
|
elem.key = Object.assign( { $inferred: 'include' }, origin.key );
|
|
923
|
+
if (origin.value && origin.$syntax === 'calc') {
|
|
924
|
+
// TODO: If paths become invalid in the new artifact, should we mark
|
|
925
|
+
// all usages in the expressions? Possibly just the first one?
|
|
926
|
+
elem.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
|
|
927
|
+
elem.$syntax = 'calc';
|
|
928
|
+
setLink( elem, '_calcOrigin', origin._calcOrigin || origin );
|
|
929
|
+
}
|
|
746
930
|
// TODO: also complain if elem is just defined in art
|
|
747
931
|
});
|
|
748
932
|
}
|
|
749
933
|
}
|
|
934
|
+
checkRedefinitionThroughIncludes( parent, prop );
|
|
750
935
|
// TODO: expand elements having direct elements (if needed)
|
|
751
936
|
if (members) {
|
|
752
937
|
forEachInOrder( { [prop]: members }, prop, ( elem, name ) => {
|
|
@@ -755,6 +940,31 @@ function extend( model ) {
|
|
|
755
940
|
}
|
|
756
941
|
}
|
|
757
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Report duplicates in parent[prop] that happen due to multiple includes having the
|
|
945
|
+
* same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
|
|
946
|
+
*
|
|
947
|
+
* TODO(v4): Make this a hard error; see checkRedefinition(); maybe combine both;
|
|
948
|
+
*/
|
|
949
|
+
function checkRedefinitionThroughIncludes( parent, prop ) {
|
|
950
|
+
if (!parent[prop])
|
|
951
|
+
return;
|
|
952
|
+
forEachInOrder(parent, prop, ( member, name ) => {
|
|
953
|
+
if (member.$inferred === 'include' && Array.isArray(member.$duplicates)) {
|
|
954
|
+
const includes = [ member, ...member.$duplicates ].map(dup => dup._origin._main);
|
|
955
|
+
if (isBetaEnabled(options, 'v4preview')) {
|
|
956
|
+
error( 'duplicate-definition', [ parent.name.location, member ],
|
|
957
|
+
{ '#': `include-${ prop }`, name, sorted_arts: includes } );
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
// Error accidentally removed in v2/v3, therefore only a warning.
|
|
961
|
+
warning( 'ref-duplicate-include-member', [ parent.name.location, member ],
|
|
962
|
+
{ '#': prop, name, sorted_arts: includes } );
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
758
968
|
// localized texts entities
|
|
759
969
|
|
|
760
970
|
/**
|
|
@@ -881,6 +1091,7 @@ function extend( model ) {
|
|
|
881
1091
|
const art = useTextsAspect
|
|
882
1092
|
? createTextsEntityWithInclude( base, absolute, fioriEnabled )
|
|
883
1093
|
: createTextsEntityWithDefaultElements( base, absolute, fioriEnabled );
|
|
1094
|
+
// both functions are rather similar...
|
|
884
1095
|
|
|
885
1096
|
const { location } = base.name;
|
|
886
1097
|
|
|
@@ -916,7 +1127,7 @@ function extend( model ) {
|
|
|
916
1127
|
elem.key = { val: true, $inferred: 'localized', location };
|
|
917
1128
|
// If the propagated elements remain key (that is not fiori.draft.enabled)
|
|
918
1129
|
// they should be omitted from OData containment EDM
|
|
919
|
-
|
|
1130
|
+
setAnnotation( elem, '@odata.containment.ignore', location );
|
|
920
1131
|
}
|
|
921
1132
|
else {
|
|
922
1133
|
// add the former key paths to the unique constraint
|
|
@@ -953,10 +1164,10 @@ function extend( model ) {
|
|
|
953
1164
|
path: [ { id: locale.name.id, location: locale.location } ],
|
|
954
1165
|
location: locale.location,
|
|
955
1166
|
});
|
|
956
|
-
|
|
1167
|
+
setAnnotation( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' );
|
|
957
1168
|
}
|
|
958
1169
|
|
|
959
|
-
copyPersistenceAnnotations(art, base
|
|
1170
|
+
copyPersistenceAnnotations( art, base );
|
|
960
1171
|
return art;
|
|
961
1172
|
}
|
|
962
1173
|
|
|
@@ -988,7 +1199,7 @@ function extend( model ) {
|
|
|
988
1199
|
if (!fioriEnabled) {
|
|
989
1200
|
// To be compatible, we switch off draft without @fiori.draft.enabled
|
|
990
1201
|
// TODO (next major version): remove?
|
|
991
|
-
|
|
1202
|
+
setAnnotation( art, '@odata.draft.enabled', art.location, false );
|
|
992
1203
|
}
|
|
993
1204
|
else {
|
|
994
1205
|
// @fiori.draft.enabled artifacts need default elements ID_texts and locale.
|
|
@@ -1009,9 +1220,11 @@ function extend( model ) {
|
|
|
1009
1220
|
|
|
1010
1221
|
if (addTextsLanguageAssoc && art.elements.language)
|
|
1011
1222
|
art.elements.language = undefined; // TODO: Message? Ignore?
|
|
1223
|
+
// TODO: what is this necessary? We do not create a text entity in this case
|
|
1012
1224
|
|
|
1013
1225
|
setLink( art, '_block', model.$internal );
|
|
1014
1226
|
model.definitions[absolute] = art;
|
|
1227
|
+
chooseAnnotationsInArtifact( art ); // having extensions here would be wrong
|
|
1015
1228
|
return art;
|
|
1016
1229
|
}
|
|
1017
1230
|
|
|
@@ -1046,7 +1259,7 @@ function extend( model ) {
|
|
|
1046
1259
|
locale.key = { val: true, location };
|
|
1047
1260
|
// To be compatible, we switch off draft without @fiori.draft.enabled
|
|
1048
1261
|
// TODO (next major version): remove?
|
|
1049
|
-
|
|
1262
|
+
setAnnotation( art, '@odata.draft.enabled', art.location, false );
|
|
1050
1263
|
}
|
|
1051
1264
|
else {
|
|
1052
1265
|
const textId = {
|
|
@@ -1058,11 +1271,11 @@ function extend( model ) {
|
|
|
1058
1271
|
};
|
|
1059
1272
|
dictAdd( art.elements, 'ID_texts', textId );
|
|
1060
1273
|
}
|
|
1061
|
-
|
|
1062
1274
|
dictAdd( art.elements, 'locale', locale );
|
|
1275
|
+
|
|
1063
1276
|
setLink( art, '_block', model.$internal );
|
|
1064
1277
|
model.definitions[absolute] = art;
|
|
1065
|
-
|
|
1278
|
+
chooseAnnotationsInArtifact( art ); // having extensions here would be wrong
|
|
1066
1279
|
return art;
|
|
1067
1280
|
}
|
|
1068
1281
|
|
|
@@ -1346,9 +1559,9 @@ function extend( model ) {
|
|
|
1346
1559
|
model.definitions[entityName] = art;
|
|
1347
1560
|
initArtifact( art );
|
|
1348
1561
|
|
|
1562
|
+
chooseAnnotationsInArtifact( art ); // having extensions here would be wrong
|
|
1349
1563
|
// Copy persistence annotations from aspect.
|
|
1350
|
-
copyPersistenceAnnotations(art, target
|
|
1351
|
-
|
|
1564
|
+
copyPersistenceAnnotations( art, target ); // after chooseAnnotation()
|
|
1352
1565
|
return art;
|
|
1353
1566
|
}
|
|
1354
1567
|
|
|
@@ -1364,57 +1577,361 @@ function extend( model ) {
|
|
|
1364
1577
|
if (origin.key)
|
|
1365
1578
|
proxy.key = Object.assign( { $inferred: 'include' }, origin.key );
|
|
1366
1579
|
if (anno)
|
|
1367
|
-
|
|
1580
|
+
setAnnotation( proxy, anno );
|
|
1368
1581
|
dictAdd( proxyDict.elements, pname, proxy );
|
|
1369
1582
|
}
|
|
1370
1583
|
}
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
// Phase 4 - annotations ---------------------------------------------------
|
|
1587
|
+
// move to top
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Goes through all (applied) annotations in the given artifact and chooses one
|
|
1591
|
+
* if multiple exist according to the module layer.
|
|
1592
|
+
* TODO: rename to extendArtifactBefore
|
|
1593
|
+
*
|
|
1594
|
+
* @param {XSN.Artifact} art
|
|
1595
|
+
*/
|
|
1596
|
+
function chooseAnnotationsInArtifact( art ) {
|
|
1597
|
+
// for main artifacts, move extensions from `$lateExtensions` model dictionary:
|
|
1598
|
+
if (!art._main && !art._outer && art._extensions === undefined &&
|
|
1599
|
+
art.kind !== 'namespace') {
|
|
1600
|
+
// if (!art.name) console.log(art)
|
|
1601
|
+
const { absolute } = art.name;
|
|
1602
|
+
setLink( art, '_extensions', model.$lateExtensions[absolute]?._extensions || null );
|
|
1603
|
+
if (art._extensions && !art.builtin) { // keep extensions for builtin in $lateExtensions
|
|
1604
|
+
delete model.$lateExtensions[absolute];
|
|
1605
|
+
// TODO: if the extension mechanism has been completed, we could uncomment:
|
|
1606
|
+
// art._extensions.forEach( ext => resolvePath( ext.name, ext.kind, ext )); // for LSP
|
|
1607
|
+
// (if we do it now, the old extend functionality might think “already applied”)
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (art._extensions) {
|
|
1611
|
+
// TODO: the following function can now be simplified
|
|
1612
|
+
// if (art.$inferred) console.log('CAI:', art.name, art.$inferred,art._extensions)
|
|
1613
|
+
// With extensions, member appears in CSN, affects directly the rendering of
|
|
1614
|
+
// elements etc. TODO: do that more specifically on the dicts (via symbol)
|
|
1615
|
+
// Probably better: we could use the _extensions dict prop directly in to-csn
|
|
1616
|
+
if (art.$inferred)
|
|
1617
|
+
setExpandStatusAnnotate( art, 'annotate' );
|
|
1618
|
+
if (Array.isArray( art._extensions )) {
|
|
1619
|
+
checkExtensionsKind( art._extensions, art ); // TODO: check with builtins
|
|
1620
|
+
transformArtifactExtensions( art );
|
|
1621
|
+
}
|
|
1622
|
+
applyAllExtensions( art );
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// TODO: if extensions has more than one of returns,items,elements,enum, delete all those props
|
|
1627
|
+
function transformArtifactExtensions( art ) {
|
|
1628
|
+
const hasOnlySubExtensions = art._outer; // items, anonymous aspects
|
|
1629
|
+
const dict = Object.create(null);
|
|
1630
|
+
for (const ext of art._extensions) {
|
|
1631
|
+
for (const prop in ext) {
|
|
1632
|
+
if (ext[prop] === undefined) // deleted propery
|
|
1633
|
+
continue;
|
|
1634
|
+
// TODO: do this check nicer (after complete move to new extensions mechanism)
|
|
1635
|
+
if (prop.charAt(0) === '@' || prop === 'doc' ||
|
|
1636
|
+
prop === 'includes' || prop === 'columns' ||
|
|
1637
|
+
prop === 'length' || prop === 'scale' || prop === 'precision' || prop === 'srid') {
|
|
1638
|
+
if (!hasOnlySubExtensions)
|
|
1639
|
+
pushToDict( dict, prop, ext );
|
|
1640
|
+
}
|
|
1641
|
+
else if (prop === 'elements' || prop === 'enum' || prop === 'actions' ||
|
|
1642
|
+
prop === 'params' || prop === 'returns') {
|
|
1643
|
+
if (ext.kind === 'extend')
|
|
1644
|
+
pushToDict( dict, 'includes', ext );
|
|
1645
|
+
pushToDict( dict, prop, ext );
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
art._extensions = dict;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function applyAllExtensions( art ) {
|
|
1653
|
+
const extensions = art._extensions;
|
|
1654
|
+
for (const prop in extensions) {
|
|
1655
|
+
// TODO: do the following `if` in a nicer way
|
|
1656
|
+
if ([ 'elements', 'enum', 'actions', 'params', 'returns' ].includes( prop ))
|
|
1657
|
+
continue; // currently just annotates on sub elements - TODO: error here
|
|
1658
|
+
// annotations, `doc`, `includes`, `columns`, `length`, ...
|
|
1659
|
+
const scheduled = [];
|
|
1660
|
+
// sort extensions according to layer (specified elements are bottom layer):
|
|
1661
|
+
const layered = layeredExtensions( extensions[prop] );
|
|
1662
|
+
|
|
1663
|
+
let cont = true;
|
|
1664
|
+
while (cont) {
|
|
1665
|
+
const { highest, issue } = extensionsOfHighestLayers( layered );
|
|
1666
|
+
// console.log( 'CA:', annoName, issue, extensions)
|
|
1667
|
+
let index = highest.length;
|
|
1668
|
+
cont = !!index; // safety
|
|
1669
|
+
while (--index >= 0) {
|
|
1670
|
+
const ext = highest[index];
|
|
1671
|
+
scheduled.push( ext );
|
|
1672
|
+
if (extensionOverwrites( ext, prop )) {
|
|
1673
|
+
cont = false;
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (issue || index > 0)
|
|
1678
|
+
reportDuplicateExtensions( highest, prop, issue, index, art );
|
|
1679
|
+
}
|
|
1680
|
+
// Now apply the relevant extensions
|
|
1681
|
+
scheduled.reverse();
|
|
1682
|
+
for (const ext of scheduled)
|
|
1683
|
+
applySingleExtension( art, ext, prop );
|
|
1684
|
+
delete extensions[prop];
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function extensionOverwrites( ext, prop ) {
|
|
1689
|
+
return (prop.charAt(0) !== '@')
|
|
1690
|
+
? [ 'doc', 'length', 'precision', 'scale', 'srid' ].includes( prop )
|
|
1691
|
+
: !annotationHasEllipsis( ext[prop] );
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// TODO: still a bit annotation assignment specific
|
|
1695
|
+
function reportDuplicateExtensions( extensions, prop, issue, index, art ) {
|
|
1696
|
+
// TODO: think about messages for these
|
|
1697
|
+
if (prop === 'elements' || prop === 'enum' || prop === 'actions' || prop === 'columns' ||
|
|
1698
|
+
prop === 'params' || prop === 'returns' || prop === 'includes' )
|
|
1699
|
+
return; // extensions currently handled extra
|
|
1700
|
+
if (issue) {
|
|
1701
|
+
// eslint-disable-next-line no-nested-ternary
|
|
1702
|
+
let msg = (index < 0)
|
|
1703
|
+
? 'anno-unstable-array'
|
|
1704
|
+
: (issue === true)
|
|
1705
|
+
? 'anno-duplicate'
|
|
1706
|
+
: 'anno-duplicate-unrelated-layer';
|
|
1707
|
+
if (prop.charAt(0) !== '@' && prop !== 'doc') {
|
|
1708
|
+
msg = (issue === true)
|
|
1709
|
+
? 'ext-duplicate-extend-type'
|
|
1710
|
+
: 'ext-duplicate-extend-type-unrelated-layer';
|
|
1711
|
+
// not sure whether to repeat the extended artifact in the message (we
|
|
1712
|
+
// have the semantic location, after all)
|
|
1713
|
+
}
|
|
1714
|
+
const variant = prop === 'doc' ? 'doc' : 'std';
|
|
1715
|
+
for (const ext of extensions) {
|
|
1716
|
+
const anno = ext[prop];
|
|
1717
|
+
if (anno && !anno.$errorReported) {
|
|
1718
|
+
message( msg, [ anno.name?.location || anno.location, ext ],
|
|
1719
|
+
{ '#': variant, anno: prop, type: art } );
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
else if (index > 0) { // more than one set (not just ...)
|
|
1724
|
+
const variant = prop === 'doc' ? 'doc' : 'std';
|
|
1725
|
+
const msgid = (prop.charAt(0) === '@' || prop === 'doc')
|
|
1726
|
+
? 'anno-duplicate-same-file' // TODO: always ext-duplicate-…
|
|
1727
|
+
: 'ext-duplicate-same-file';
|
|
1728
|
+
while (index >= 0) { // do not report for trailing [...]
|
|
1729
|
+
const ext = extensions[index--];
|
|
1730
|
+
const anno = ext[prop];
|
|
1731
|
+
warning( msgid, [ anno.name?.location || anno.location, ext ],
|
|
1732
|
+
{ '#': variant, prop, anno: prop } );
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function applySingleExtension( art, ext, prop ) {
|
|
1738
|
+
if (prop === 'includes') {
|
|
1739
|
+
if (ext.kind === 'extend' && art.$inferred) {
|
|
1740
|
+
error( 'extend-for-generated', [ ext.name.location, ext ], { art },
|
|
1741
|
+
'You can\'t use EXTEND on the generated $(ART)' );
|
|
1742
|
+
}
|
|
1743
|
+
else if (art.kind !== 'annotate' && !art._outer) { // not with elem extension in targetAspect
|
|
1744
|
+
const { absolute } = art.name;
|
|
1745
|
+
const dict = extensionsDict[absolute] || (extensionsDict[absolute] = []);
|
|
1746
|
+
dict.push( ext ); // TODO: change
|
|
1747
|
+
// console.log( 'ASI:',prop,art.name,ext,extensionsDict[absolute])
|
|
1748
|
+
}
|
|
1749
|
+
// art[prop] = (art[prop]) ? art[prop].concat( ext[prop] ) : ext[prop];
|
|
1750
|
+
}
|
|
1751
|
+
else if (prop === 'columns') {
|
|
1752
|
+
const { query } = art;
|
|
1753
|
+
if (!query?.from?.path)
|
|
1754
|
+
error( 'extend-columns', [ ext.columns[$location], ext ], { art } );
|
|
1755
|
+
else if (!query.columns)
|
|
1756
|
+
query.columns = [ { location: query.from.location, val: '*' }, ...ext.columns ];
|
|
1757
|
+
else
|
|
1758
|
+
query.columns.push( ...ext.columns );
|
|
1759
|
+
}
|
|
1760
|
+
else if ([ 'length', 'precision', 'scale', 'srid' ].includes( prop )) {
|
|
1761
|
+
const typeExts = art.$typeExts || (art.$typeExts = {});
|
|
1762
|
+
typeExts[prop] = ext;
|
|
1763
|
+
}
|
|
1764
|
+
else {
|
|
1765
|
+
const result = applyAssignment( art[prop], ext[prop], ext, prop );
|
|
1766
|
+
art[prop] = (result.name) ? result : Object.assign( {}, art[prop], result );
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function applyAssignment( previousAnno, anno, art, annoName ) {
|
|
1771
|
+
const firstEllipsis = annotationHasEllipsis( anno );
|
|
1772
|
+
if (!firstEllipsis)
|
|
1773
|
+
return anno;
|
|
1774
|
+
const hasBase = previousAnno?.literal === 'array';
|
|
1775
|
+
if (!previousAnno) {
|
|
1776
|
+
const loc = firstEllipsis.location || anno.name.location;
|
|
1777
|
+
message( 'anno-unexpected-ellipsis', [ loc, art ], { code: '...' } );
|
|
1778
|
+
previousAnno = { val: [] };
|
|
1779
|
+
}
|
|
1780
|
+
else if (previousAnno.literal !== 'array') {
|
|
1781
|
+
// TODO: If we introduce sub-messages, point to the non-array base value.
|
|
1782
|
+
error( 'anno-mismatched-ellipsis', [ anno.name.location, art ], { code: '...' } );
|
|
1783
|
+
previousAnno = { val: [] };
|
|
1784
|
+
}
|
|
1785
|
+
const previousValue = previousAnno.val;
|
|
1786
|
+
let prevPos = 0;
|
|
1787
|
+
const result = [];
|
|
1788
|
+
for (const item of anno.val) {
|
|
1789
|
+
const ell = item && item.literal === 'token' && item.val === '...';
|
|
1790
|
+
if (!ell) {
|
|
1791
|
+
result.push( item );
|
|
1792
|
+
}
|
|
1793
|
+
else {
|
|
1794
|
+
let upToSpec = item.upTo && checkUpToSpec( item.upTo, art, annoName, true );
|
|
1795
|
+
while (prevPos < previousValue.length) {
|
|
1796
|
+
const prevItem = previousValue[prevPos++];
|
|
1797
|
+
result.push( prevItem );
|
|
1798
|
+
if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) {
|
|
1799
|
+
upToSpec = false;
|
|
1800
|
+
break;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (upToSpec && hasBase) {
|
|
1804
|
+
// non-matched UP TO; if there is no base to apply to, there is already an error.
|
|
1805
|
+
warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' },
|
|
1806
|
+
'The $(CODE) value does not match any item in the base annotation $(ANNO)' );
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
// console.log('TP:',previousValue.map(se),anno.val.map(se),'->',result.map(se))
|
|
1811
|
+
return { val: result, literal: 'array' };
|
|
1812
|
+
}
|
|
1813
|
+
// function se(a) { return a.upTo ? [a.val,a.upTo.val] : a.val ; }
|
|
1814
|
+
|
|
1815
|
+
function checkUpToSpec( upToSpec, art, annoName, isFullUpTo ) {
|
|
1816
|
+
const { literal } = upToSpec;
|
|
1817
|
+
if (!isFullUpTo) { // inside struct of UP TO
|
|
1818
|
+
if (literal !== 'struct' && literal !== 'array' )
|
|
1819
|
+
return true;
|
|
1820
|
+
}
|
|
1821
|
+
else if (literal === 'struct') {
|
|
1822
|
+
return Object.values( upToSpec.struct ).every( v => checkUpToSpec( v, art, annoName ) );
|
|
1823
|
+
}
|
|
1824
|
+
else if (literal !== 'array' && literal !== 'boolean' && literal !== 'null') {
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
error( null, [ upToSpec.location, art ],
|
|
1828
|
+
{ anno: annoName, code: '... up to', '#': literal },
|
|
1829
|
+
{
|
|
1830
|
+
std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)',
|
|
1831
|
+
array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)',
|
|
1832
|
+
// eslint-disable-next-line max-len
|
|
1833
|
+
struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)',
|
|
1834
|
+
boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)',
|
|
1835
|
+
null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)',
|
|
1836
|
+
} );
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function equalUpTo( previousItem, upToSpec ) {
|
|
1841
|
+
if (!previousItem)
|
|
1842
|
+
return false;
|
|
1843
|
+
if ('val' in upToSpec) {
|
|
1844
|
+
if (previousItem.val === upToSpec.val) // enum, struct and ref have no val
|
|
1845
|
+
return true;
|
|
1846
|
+
const typeUpTo = typeof upToSpec.val;
|
|
1847
|
+
const typePrev = typeof previousItem.val;
|
|
1848
|
+
if (typeUpTo === 'number')
|
|
1849
|
+
return typePrev === 'string' && previousItem.val === upToSpec.val.toString();
|
|
1850
|
+
if (typePrev === 'number')
|
|
1851
|
+
return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString();
|
|
1852
|
+
}
|
|
1853
|
+
else if (upToSpec.path) {
|
|
1854
|
+
return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec );
|
|
1855
|
+
}
|
|
1856
|
+
else if (upToSpec.sym) {
|
|
1857
|
+
return previousItem.sym && previousItem.sym.id === upToSpec.sym.id;
|
|
1858
|
+
}
|
|
1859
|
+
else if (upToSpec.struct && previousItem.struct) {
|
|
1860
|
+
return Object.entries( upToSpec.struct )
|
|
1861
|
+
.every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) );
|
|
1862
|
+
}
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function normalizeRef( node ) { // see to-csn.js
|
|
1867
|
+
const ref = pathName( node.path );
|
|
1868
|
+
return node.variant ? `${ ref }#${ node.variant.id }` : ref;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from
|
|
1873
|
+
* source to target if present on source but not target.
|
|
1874
|
+
*
|
|
1875
|
+
* @param {object} target
|
|
1876
|
+
* @param {object} source
|
|
1877
|
+
*/
|
|
1878
|
+
function copyPersistenceAnnotations( target, source ) {
|
|
1879
|
+
if (!source)
|
|
1880
|
+
return;
|
|
1881
|
+
|
|
1882
|
+
const copyExists = !isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' );
|
|
1883
|
+
if (copyExists)
|
|
1884
|
+
copy( '@cds.persistence.exists' );
|
|
1885
|
+
copy( '@cds.persistence.skip' );
|
|
1886
|
+
|
|
1887
|
+
function copy( anno ) {
|
|
1888
|
+
if ( source[anno] && !target[anno] )
|
|
1889
|
+
target[anno] = { ...source[anno], $inferred: 'parent-origin' };
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1371
1892
|
}
|
|
1372
1893
|
|
|
1373
1894
|
/**
|
|
1374
|
-
* Group
|
|
1895
|
+
* Group extensions by their layers. A definition (for specified elements)
|
|
1375
1896
|
* is considered to be provided in a layer named '', the lowest layer.
|
|
1376
1897
|
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
* @param {object[]} assignments Array of assignments, e.g. extensions.
|
|
1381
|
-
* @returns {Record<string, object>} key: layer name, value: {name, layer, assignments[]}`
|
|
1898
|
+
* @param {object[]} extensions Array of extensions.
|
|
1899
|
+
* @returns {Record<string, object>} key: layer name, value: {name, layer, extensions[]}`
|
|
1382
1900
|
*/
|
|
1383
|
-
function
|
|
1901
|
+
function layeredExtensions( extensions ) {
|
|
1384
1902
|
const layered = Object.create(null);
|
|
1385
|
-
for (const
|
|
1386
|
-
const layer =
|
|
1903
|
+
for (const ext of extensions) {
|
|
1904
|
+
const layer = (ext.kind === 'annotate' || ext.kind === 'extend') && layers.layer( ext );
|
|
1387
1905
|
// just consider layer if Extend/Annotate, not Define
|
|
1388
1906
|
const name = (layer) ? layer.realname : '';
|
|
1389
1907
|
const done = layered[name];
|
|
1390
1908
|
if (done)
|
|
1391
|
-
done.
|
|
1909
|
+
done.extensions.push( ext );
|
|
1392
1910
|
else
|
|
1393
|
-
layered[name] = { name, layer,
|
|
1394
|
-
// TODO: file - if set: unique in layer
|
|
1911
|
+
layered[name] = { name, layer, extensions: [ ext ] };
|
|
1395
1912
|
}
|
|
1396
1913
|
return layered;
|
|
1397
1914
|
}
|
|
1398
1915
|
|
|
1399
1916
|
/**
|
|
1400
|
-
* Return
|
|
1917
|
+
* Return extensions of the highest layers.
|
|
1401
1918
|
* Also returns whether there could be an issue:
|
|
1402
|
-
* - false: there
|
|
1403
|
-
* - 'unrelated': there is just one
|
|
1404
|
-
* - true: there is at least one layer with two or more
|
|
1405
|
-
*
|
|
1919
|
+
* - false: there are just extensions in one file,
|
|
1920
|
+
* - 'unrelated': there is just one extension per layer
|
|
1921
|
+
* - true: there is at least one layer with two or more extensions, and
|
|
1922
|
+
* at least two files are involved
|
|
1406
1923
|
*
|
|
1407
|
-
* @param {Record<string, object>}
|
|
1924
|
+
* @param {Record<string, object>} layered Structure as returned by layeredExtensions()
|
|
1408
1925
|
* @returns {{assignments, issue: boolean|string}}
|
|
1409
1926
|
*/
|
|
1410
|
-
function
|
|
1411
|
-
const layerNames = Object.keys(
|
|
1927
|
+
function extensionsOfHighestLayers( layered ) {
|
|
1928
|
+
const layerNames = Object.keys( layered );
|
|
1412
1929
|
// console.log('HIB:',layerNames)
|
|
1413
1930
|
if (layerNames.length <= 1) {
|
|
1414
1931
|
const name = layerNames[0];
|
|
1415
|
-
const
|
|
1416
|
-
delete
|
|
1417
|
-
return {
|
|
1932
|
+
const highest = layered[name]?.extensions || [];
|
|
1933
|
+
delete layered[name];
|
|
1934
|
+
return { highest, issue: inMoreThanOneFile( highest ) };
|
|
1418
1935
|
}
|
|
1419
1936
|
|
|
1420
1937
|
// collect all layers which are lower than another layer
|
|
@@ -1422,28 +1939,41 @@ function assignmentsOfHighestLayers( layeredAnnos ) {
|
|
|
1422
1939
|
allExtends[''] = {}; // the "Define" layer
|
|
1423
1940
|
for (const name of layerNames) {
|
|
1424
1941
|
if (name) // not the "Define" layer
|
|
1425
|
-
Object.assign( allExtends,
|
|
1942
|
+
Object.assign( allExtends, layered[name].layer._layerExtends );
|
|
1426
1943
|
}
|
|
1427
1944
|
// console.log('HIE:',Object.keys(allExtends))
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1945
|
+
const highest = []; // extensions
|
|
1946
|
+
const highestLayers = [];
|
|
1430
1947
|
for (const name of layerNames) {
|
|
1431
1948
|
if (!(name in allExtends)) {
|
|
1432
|
-
const layer =
|
|
1433
|
-
delete
|
|
1434
|
-
|
|
1435
|
-
|
|
1949
|
+
const layer = layered[name];
|
|
1950
|
+
delete layered[name];
|
|
1951
|
+
highestLayers.push( layer );
|
|
1952
|
+
highest.push( ...layer.extensions );
|
|
1436
1953
|
}
|
|
1437
1954
|
}
|
|
1438
|
-
|
|
1439
|
-
const good =
|
|
1955
|
+
highest.sort( compareExtensions );
|
|
1956
|
+
const good = highestLayers.every( layer => !inMoreThanOneFile( layer.extensions ));
|
|
1440
1957
|
// TODO: use layer.file instead
|
|
1441
|
-
const issue = !good ||
|
|
1442
|
-
// console.log('HI:',highest.map(l=>l.name),issue,issue&&
|
|
1443
|
-
return {
|
|
1958
|
+
const issue = !good || highestLayers.length > 1 && 'unrelated';
|
|
1959
|
+
// console.log('HI:',highest.map(l=>l.name),issue,issue&&extensions)
|
|
1960
|
+
return { highest, issue };
|
|
1444
1961
|
}
|
|
1445
1962
|
|
|
1446
|
-
function
|
|
1963
|
+
function inMoreThanOneFile( extensions ) {
|
|
1964
|
+
if (extensions.length <= 1)
|
|
1965
|
+
return false;
|
|
1966
|
+
const file = extensions[0].location?.file;
|
|
1967
|
+
return !file || extensions.slice(1).some( e => e.location?.file !== file );
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* Compare two extensions which are not comparable via layering:
|
|
1972
|
+
* - via the fs.realpath of the file (not layer!) of the extensions, then
|
|
1973
|
+
* - via the line, then column of the extensions.
|
|
1974
|
+
* Returns <0 if `a`<`b`, >1 if `a`>`b`, i.e. can be used for ascending sort.
|
|
1975
|
+
*/
|
|
1976
|
+
function compareExtensions( a, b ) {
|
|
1447
1977
|
const fileA = layers.realname( a._block );
|
|
1448
1978
|
const fileB = layers.realname( b._block );
|
|
1449
1979
|
if (fileA !== fileB)
|
|
@@ -1452,29 +1982,6 @@ function compareAssignments( a, b ) {
|
|
|
1452
1982
|
(a?.location?.col || 0) - (b?.location?.col || 0);
|
|
1453
1983
|
}
|
|
1454
1984
|
|
|
1455
|
-
/**
|
|
1456
|
-
* Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from
|
|
1457
|
-
* source to target if present on source but not target.
|
|
1458
|
-
*
|
|
1459
|
-
* @param {object} target
|
|
1460
|
-
* @param {object} source
|
|
1461
|
-
* @param {CSN.Options} options
|
|
1462
|
-
*/
|
|
1463
|
-
function copyPersistenceAnnotations( target, source, options ) {
|
|
1464
|
-
if (!source)
|
|
1465
|
-
return;
|
|
1466
|
-
|
|
1467
|
-
const copyExists = !isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' );
|
|
1468
|
-
if (copyExists)
|
|
1469
|
-
copy( '@cds.persistence.exists' );
|
|
1470
|
-
copy( '@cds.persistence.skip' );
|
|
1471
|
-
|
|
1472
|
-
function copy( anno ) {
|
|
1473
|
-
if ( source[anno] && !target[anno] )
|
|
1474
|
-
target[anno] = { ...source[anno], $inferred: 'parent-origin' };
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
1985
|
function augmentEqual( location, assocname, relations, prefix = '' ) {
|
|
1479
1986
|
const args = relations.map( eq );
|
|
1480
1987
|
return (args.length === 1)
|
|
@@ -1508,15 +2015,14 @@ function augmentEqual( location, assocname, relations, prefix = '' ) {
|
|
|
1508
2015
|
* @param {XSN.Extension} ext
|
|
1509
2016
|
* @param {object} art
|
|
1510
2017
|
*/
|
|
1511
|
-
function storeTypeExtension( ext, art ) {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
}
|
|
1519
|
-
|
|
2018
|
+
// function storeTypeExtension( ext, art ) {
|
|
2019
|
+
// // If there are no parameters to apply, don't store the extension.
|
|
2020
|
+
// if (!typeParameters.list.some( prop => ext[prop] !== undefined ))
|
|
2021
|
+
// return;
|
|
2022
|
+
// else if (!art._extendType)
|
|
2023
|
+
// setLink( art, '_extendType', [] );
|
|
2024
|
+
// art._extendType.push( ext );
|
|
2025
|
+
// }
|
|
1520
2026
|
|
|
1521
2027
|
function checkTextsLanguageAssocOption( model, options ) {
|
|
1522
2028
|
const languages = model.definitions['sap.common.Languages'];
|