@sap/cds-compiler 4.0.2 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +100 -5
  2. package/bin/cdsc.js +12 -12
  3. package/doc/CHANGELOG_BETA.md +11 -0
  4. package/lib/api/main.js +31 -11
  5. package/lib/api/validate.js +1 -1
  6. package/lib/base/location.js +6 -7
  7. package/lib/base/message-registry.js +84 -38
  8. package/lib/base/messages.js +11 -10
  9. package/lib/base/model.js +6 -2
  10. package/lib/checks/defaultValues.js +6 -6
  11. package/lib/checks/foreignKeys.js +0 -5
  12. package/lib/checks/onConditions.js +17 -12
  13. package/lib/checks/queryNoDbArtifacts.js +132 -72
  14. package/lib/checks/sql-snippets.js +15 -4
  15. package/lib/checks/types.js +3 -3
  16. package/lib/checks/utils.js +1 -1
  17. package/lib/compiler/assert-consistency.js +44 -16
  18. package/lib/compiler/base.js +1 -0
  19. package/lib/compiler/builtins.js +7 -8
  20. package/lib/compiler/checks.js +274 -197
  21. package/lib/compiler/classes.js +62 -0
  22. package/lib/compiler/cycle-detector.js +3 -3
  23. package/lib/compiler/define.js +63 -50
  24. package/lib/compiler/extend.js +38 -20
  25. package/lib/compiler/finalize-parse-cdl.js +2 -1
  26. package/lib/compiler/generate.js +0 -8
  27. package/lib/compiler/index.js +9 -7
  28. package/lib/compiler/kick-start.js +2 -0
  29. package/lib/compiler/populate.js +139 -110
  30. package/lib/compiler/propagator.js +4 -3
  31. package/lib/compiler/resolve.js +157 -126
  32. package/lib/compiler/shared.js +706 -404
  33. package/lib/compiler/tweak-assocs.js +21 -10
  34. package/lib/compiler/utils.js +228 -36
  35. package/lib/edm/annotations/genericTranslation.js +1 -1
  36. package/lib/edm/edm.js +4 -1
  37. package/lib/edm/edmPreprocessor.js +5 -4
  38. package/lib/edm/edmUtils.js +2 -4
  39. package/lib/gen/Dictionary.json +34 -10
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +1 -1
  42. package/lib/gen/languageParser.js +3987 -3963
  43. package/lib/json/from-csn.js +43 -47
  44. package/lib/json/to-csn.js +11 -11
  45. package/lib/language/antlrParser.js +2 -1
  46. package/lib/language/genericAntlrParser.js +52 -43
  47. package/lib/language/language.g4 +59 -59
  48. package/lib/language/multiLineStringParser.js +2 -0
  49. package/lib/main.d.ts +5 -0
  50. package/lib/model/csnRefs.js +37 -19
  51. package/lib/model/csnUtils.js +20 -16
  52. package/lib/model/revealInternalProperties.js +29 -21
  53. package/lib/modelCompare/compare.js +112 -39
  54. package/lib/modelCompare/utils/filter.js +54 -24
  55. package/lib/optionProcessor.js +6 -6
  56. package/lib/render/manageConstraints.js +20 -17
  57. package/lib/render/toCdl.js +34 -20
  58. package/lib/render/toHdbcds.js +2 -2
  59. package/lib/render/toRename.js +4 -9
  60. package/lib/render/toSql.js +77 -26
  61. package/lib/render/utils/common.js +3 -3
  62. package/lib/render/utils/unique.js +52 -0
  63. package/lib/transform/db/applyTransformations.js +61 -20
  64. package/lib/transform/db/assertUnique.js +7 -8
  65. package/lib/transform/db/associations.js +2 -2
  66. package/lib/transform/db/cdsPersistence.js +8 -8
  67. package/lib/transform/db/expansion.js +17 -21
  68. package/lib/transform/db/flattening.js +23 -23
  69. package/lib/transform/db/rewriteCalculatedElements.js +20 -14
  70. package/lib/transform/db/temporal.js +1 -1
  71. package/lib/transform/db/transformExists.js +8 -7
  72. package/lib/transform/db/views.js +73 -33
  73. package/lib/transform/draft/db.js +11 -9
  74. package/lib/transform/draft/odata.js +1 -1
  75. package/lib/transform/{forOdataNew.js → forOdata.js} +6 -6
  76. package/lib/transform/forRelationalDB.js +69 -75
  77. package/lib/transform/localized.js +6 -5
  78. package/lib/transform/odata/toFinalBaseType.js +3 -3
  79. package/lib/transform/{transformUtilsNew.js → transformUtils.js} +4 -101
  80. package/lib/transform/translateAssocsToJoins.js +14 -28
  81. package/package.json +1 -1
  82. package/share/messages/check-proper-type-of.md +1 -1
  83. package/share/messages/{check-proper-type.md → def-missing-type.md} +3 -5
  84. package/share/messages/message-explanations.json +1 -1
@@ -1,33 +1,36 @@
1
- // Checks on XSN performed during compile()
2
- //
3
- // TODO: to be reworked. It is not the intention to include more checks,
4
- // rather the opposite. Therefore, this file will become smaller.
1
+ // Checks on XSN performed during compile() that are useful for the user
2
+ // but not necessary for the compiler to work.
5
3
 
6
- // Major issues so far:
4
+ // TODO: Major issues so far:
7
5
  // * Different ad-hoc value/type checks (associations, enum, ...) -
8
6
  // specify a proper one and use consistently
9
7
  // * Using name comparisons instead proper object comparisons.
10
8
  // * effectiveType issues.
11
9
  // * Often forgot to consider CSN input
12
- // * Bad message texts/locations.
13
10
 
14
11
  'use strict';
15
12
 
16
- // const { hasArtifactTypeInformation } = require('../model/csnUtils')
17
13
  const builtins = require('../compiler/builtins');
18
14
  const {
19
15
  forEachGeneric,
20
16
  forEachDefinition,
21
17
  forEachMember,
18
+ isBetaEnabled,
22
19
  } = require('../base/model');
23
20
  const { CompilerAssertion } = require('../base/error');
24
21
  const { pathName } = require('./utils');
25
22
  const { forEachMemberRecursively } = require('../model/csnUtils');
23
+ const { typeParameters } = require('./builtins');
26
24
  const $location = Symbol.for('cds.$location');
27
25
 
28
- function check( model ) { // = XSN
26
+ /**
27
+ * Run compiler checks on the given XSN model.
28
+ *
29
+ * @param {XSN.Model} model
30
+ */
31
+ function check( model ) {
29
32
  const {
30
- error, warning, message, info,
33
+ error, warning, info,
31
34
  } = model.$messageFunctions;
32
35
 
33
36
  checkSapCommonLocale( model );
@@ -47,22 +50,24 @@ function check( model ) { // = XSN
47
50
  }
48
51
 
49
52
  function checkAnnotationDefinition( art ) {
53
+ // TODO: Should we check elements similar to definition-elements as well?
50
54
  checkEnumType( art );
51
55
  forEachMemberRecursively( art, (member) => {
52
56
  if (member.localized?.val)
53
57
  warning( 'def-unexpected-localized-anno', [ member.localized.location, member ] );
54
58
  });
55
- // TODO: Should we check elements similar to definition-elements as well?
56
59
  }
57
60
 
58
61
  function checkGenericConstruct( art ) {
59
62
  checkName( art );
63
+ checkTypeArguments( art );
60
64
  if (model.vocabularies) {
61
65
  Object.keys( art )
62
- .filter( a => a.startsWith('@') )
66
+ .filter( a => a.startsWith( '@' ) )
63
67
  .forEach( a => checkAnnotationAssignment1( art, art[a] ) );
64
68
  }
65
- checkTypeStructure(art);
69
+ checkTypeStructure( art );
70
+ checkAssociation( art ); // type def could be assoc
66
71
  if (art.kind === 'enum')
67
72
  checkEnum( art );
68
73
  checkEnumType( art );
@@ -75,56 +80,128 @@ function check( model ) { // = XSN
75
80
  if (member.virtual?.val === true)
76
81
  parentProps.virtual = member.virtual;
77
82
 
78
- checkGenericConstruct(member);
83
+ checkGenericConstruct( member );
79
84
 
80
85
  if (member.kind === 'element')
81
86
  checkElement( member, parentProps );
82
87
 
83
- forEachMember( member, m => checkMember(m, parentProps) );
88
+ forEachMember( member, m => checkMember( m, parentProps ) );
84
89
  }
85
90
 
86
91
  function checkVirtualKey( elem, parentProps ) {
87
- const isKey = parentProps.key || (elem.key?.val && elem.key);
88
- const isVirtual = parentProps.virtual?.val || (elem.virtual?.val && elem.virtual);
92
+ const isKey = parentProps.key?.val || elem.key?.val;
93
+ const isVirtual = parentProps.virtual?.val || elem.virtual?.val;
89
94
  if (isKey && isVirtual) {
90
95
  error('def-unexpected-key', [ isKey.location, elem ],
91
- { '#': 'virtual', name: elem.name.element, prop: 'key' });
96
+ { '#': 'virtual', art: elem.name.element, prop: 'key' });
92
97
  }
93
98
  }
94
99
 
95
100
  function checkElement( elem, parentProps ) {
96
- checkLocalizedSubElement(elem);
97
- checkVirtualKey(elem, parentProps);
98
- checkForUnmanagedAssociationsAsKey( elem, parentProps );
101
+ checkLocalizedSubElement( elem );
102
+ checkVirtualKey( elem, parentProps );
99
103
  checkLocalizedElement( elem );
100
- checkAssociation( elem );
101
104
 
102
105
  if (elem.value) {
103
106
  if (elem._main.query)
104
- checkSelectItemValue(elem);
107
+ checkSelectItemValue( elem );
105
108
  else if (elem.$syntax === 'calc')
106
109
  checkCalculatedElementValue( elem );
107
110
  }
108
111
 
109
- checkCardinality(elem); // TODO: also for assoc types
112
+ checkCardinality( elem ); // TODO: also for assoc types
110
113
  }
111
114
 
112
115
 
113
116
  function checkName( construct ) { // TODO: move to define.js
114
117
  if (model.options.$skipNameCheck)
115
118
  return;
116
- // TODO: Move a corrected version of this check to definer (but do not rely
117
- // on it!): The code below misses to consider CSN input.
118
- if (construct.name.id && construct.name.id.indexOf('.') !== -1) {
119
- // TODO: No, we should not forbid this
119
+ // TODO: Move a corrected version of this check to definer (but do not rely on it!):
120
+ // The code below misses to consider CSN input!
121
+ // Maybe remove the check? But consider runtimes that rely on '.' as element separator.
122
+ if (construct.name.id?.includes( '.' )) {
120
123
  error(null, [ construct.name.location, construct ], {},
121
124
  'The character \'.\' is not allowed in identifiers');
122
125
  }
123
126
  }
124
127
 
128
+
129
+ /**
130
+ * Check the type arguments on `art`, e.g. cds.Decimal can't have a `length`, structures
131
+ * can't have `precision`, etc.
132
+ *
133
+ * @param {XSN.Artifact} art
134
+ * @param {XSN.Artifact} user
135
+ */
136
+ function checkTypeArguments( art, user = art ) {
137
+ if (art.builtin || art.kind === 'context' || art.kind === 'service')
138
+ return;
139
+
140
+ if (art.items)
141
+ checkTypeArguments( art.items, art );
142
+
143
+ const actualParams = typeParameters.list.filter(param => art[param] !== undefined);
144
+ if (actualParams.length === 0)
145
+ return;
146
+
147
+ const typeArt = art.type?._artifact || art;
148
+
149
+ // Note: `_effectiveType` points to `art` itself, if it is an enum type,
150
+ // descend to the origin in this case.
151
+ let effectiveType = typeArt._effectiveType;
152
+ while (effectiveType?.enum)
153
+ effectiveType = (effectiveType._origin || effectiveType.type?._artifact)?._effectiveType;
154
+
155
+ if (!effectiveType) {
156
+ return; // e.g. illegal definition references, cycles, ...
157
+ }
158
+ else if (!art.type && !effectiveType.type && !effectiveType?.builtin) {
159
+ error('type-missing-type', [ art.location, user ],
160
+ { otherprop: 'type', prop: actualParams[0] },
161
+ 'Missing $(OTHERPROP) property next to $(PROP)');
162
+ return;
163
+ }
164
+
165
+ const expectedParams = effectiveType.parameters &&
166
+ effectiveType.parameters.map(p => p.name || p) || [];
167
+
168
+ for (const param of actualParams) {
169
+ if (!expectedParams.includes(param)) {
170
+ // Whether the type ref itself is a builtin or a custom type with a builtin as base.
171
+ let variant;
172
+ if ((art.type?._artifact || art._effectiveType).builtin)
173
+ variant = 'builtin';
174
+ else if (effectiveType.builtin)
175
+ variant = 'type';
176
+ else // effectiveType is not a builtin -> array or structured
177
+ variant = 'non-scalar';
178
+
179
+ error('type-unexpected-argument', [ art[param].location, user ], {
180
+ '#': variant, prop: param, art: art.type || art._effectiveType, type: effectiveType,
181
+ });
182
+ break; // Avoid spam: Only emit the first error.
183
+ }
184
+ else if (!typeParameters.expectedLiteralsFor[param].includes(art[param].literal)) {
185
+ error('type-unexpected-argument', [ art[param].location, user ], {
186
+ '#': 'incorrect-type',
187
+ prop: param,
188
+ code: art[param].literal,
189
+ names: typeParameters.expectedLiteralsFor[param],
190
+ });
191
+ break; // Avoid spam: Only emit the first error.
192
+ }
193
+ }
194
+ }
195
+
196
+ function requireExplicitTypeInSqlCast( art, user ) {
197
+ if (!art.type) {
198
+ error('expr-missing-type', [ art.location, user ], { },
199
+ 'Missing type in SQL cast function');
200
+ }
201
+ }
202
+
125
203
  function checkLocalizedElement( elem ) {
126
- // if it is directly a localized element
127
- if (elem.localized && elem.localized.val) {
204
+ if (elem.localized?.val) {
128
205
  const type = elem._effectiveType;
129
206
  // See discussion issue #6520: should we allow all scalar types?
130
207
  if (!type || !type.builtin || type.category !== 'string') {
@@ -133,14 +210,14 @@ function check( model ) { // = XSN
133
210
  'Expecting a string type in combination with keyword $(KEYWORD)');
134
211
  }
135
212
  }
136
- // "key" keyword at localized element in SELECT list.
213
+
137
214
  // TODO: This check should be moved to localized.js
138
- if (elem.key && elem.key.val && elem._main && elem._main.query) {
215
+ // "key" keyword at localized element in SELECT list.
216
+ if (elem.key?.val && elem._main?.query) {
139
217
  // original element is localized but not key, as that would have
140
- // already resulted in a warning
141
- if (elem._origin && elem._origin.localized && elem._origin.localized.val &&
142
- ( !elem._origin.key || !elem._origin.key.val)) {
143
- warning('localized-key', [ elem.key.location, elem ], { keyword: 'localized' },
218
+ // already resulted in a warning by localized.js
219
+ if (elem._origin?.localized?.val && !elem._origin.key?.val) {
220
+ warning('def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' },
144
221
  'Keyword $(KEYWORD) is ignored for primary keys');
145
222
  }
146
223
  }
@@ -173,8 +250,6 @@ function check( model ) { // = XSN
173
250
  }
174
251
  }
175
252
 
176
- // Individual checks -------------------------------------------------------
177
-
178
253
  /**
179
254
  * The enumNode is a single enum element and not the whole type.
180
255
  *
@@ -190,7 +265,7 @@ function check( model ) { // = XSN
190
265
  // Special handling to print a more detailed error message.
191
266
  // Other cases like `null` as enum value are handled in `checkEnumValueType()`
192
267
  if (type === 'enum') {
193
- warning('enum-value-ref', [ loc, enumNode ], {}, // TODO: make this an error in v4
268
+ warning('ref-unexpected-enum', [ loc, enumNode ], {},
194
269
  'References to other values are not allowed as enum values');
195
270
  }
196
271
  }
@@ -201,8 +276,7 @@ function check( model ) { // = XSN
201
276
  enumNode = enumNode.enum ? enumNode : enumNode.items;
202
277
  if (!enumNode || !enumNode.enum)
203
278
  return;
204
- const type = enumNode.type && enumNode.type._artifact &&
205
- enumNode.type._artifact._effectiveType;
279
+ const type = enumNode?.type?._artifact?._effectiveType;
206
280
 
207
281
  // We can't distinguish (in CSN) between these two cases:
208
282
  // type Base : String enum { b;a = 'abc'; };
@@ -212,25 +286,18 @@ function check( model ) { // = XSN
212
286
  if (!type || type.enum)
213
287
  return;
214
288
 
215
- const name = type.name.absolute;
216
-
217
289
  // All builtin types are allowed except binary and relational types.
218
290
  // The latter are "internal" types.
219
- const isBinary = builtins.isBinaryTypeName(name);
220
-
221
- if (!type.builtin || type.internal || isBinary) {
222
- let typeclass = 'std';
223
-
224
- if (isBinary)
225
- typeclass = 'binary';
226
- else if (builtins.isRelationTypeName(name))
227
- typeclass = 'relation';
291
+ if (!type.builtin || type.internal || type.category === 'binary') {
292
+ let typeClass = 'std';
293
+ if (type.category === 'binary' || type.category === 'relation')
294
+ typeClass = type.category;
228
295
  else if (type.elements)
229
- typeclass = 'struct';
296
+ typeClass = 'struct';
230
297
  else if (type.items)
231
- typeclass = 'items';
298
+ typeClass = 'items';
232
299
 
233
- error('enum-invalid-type', [ enumNode.type.location, enumNode ], { '#': typeclass }, {
300
+ error('type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, {
234
301
  std: 'Only builtin types are allowed as enums',
235
302
  binary: 'Binary types are not allowed as enums',
236
303
  relation: 'Relational types are not allowed as enums',
@@ -244,32 +311,32 @@ function check( model ) { // = XSN
244
311
  }
245
312
 
246
313
  /**
247
- * Check the given enum's elements and their values. For example
314
+ * Check the given enum's elements and their values. For example,
248
315
  * whether the value types are valid for the used enum type.
249
316
  * `enumNode` can be also be `type.items` if the type is an arrayed enum.
250
317
  *
251
318
  * @param {XSN.Definition} enumNode
252
319
  */
253
320
  function checkEnumValue( enumNode ) {
254
- const type = enumNode.type && enumNode.type._artifact &&
255
- enumNode.type._artifact._effectiveType;
256
- if (!enumNode.enum || !type || !type.builtin)
321
+ const type = enumNode.type?._artifact?._effectiveType;
322
+ if (!type || !enumNode.enum || !type.builtin)
257
323
  return;
258
324
 
259
- const isNumeric = builtins.isNumericTypeName(type.name.absolute);
260
- const isString = builtins.isStringTypeName(type.name.absolute);
325
+ const isNumeric = type.category === 'decimal' || type.category === 'integer';
326
+ const isString = type.category === 'string';
261
327
 
262
328
  if (!isString) {
263
329
  // Non-string enums MUST have a value as the value is only deducted for string types.
264
- const emptyValue = Object.keys(enumNode.enum).find(name => !enumNode.enum[name].value);
330
+ const emptyValue = Object.keys(enumNode.enum)
331
+ .find(name => !enumNode.enum[name].value);
265
332
  if (emptyValue) {
266
333
  const failedEnum = enumNode.enum[emptyValue];
267
- warning('enum-missing-value', [ failedEnum.location, failedEnum ],
268
- { '#': isNumeric ? 'numeric' : 'std', name: emptyValue },
269
- {
270
- std: 'Missing value for non-string enum element $(NAME)',
271
- numeric: 'Missing value for numeric enum element $(NAME)',
272
- });
334
+ warning('type-missing-value', [ failedEnum.location, failedEnum ], {
335
+ '#': isNumeric ? 'numeric' : 'std', name: emptyValue,
336
+ }, {
337
+ std: 'Missing value for non-string enum element $(NAME)',
338
+ numeric: 'Missing value for numeric enum element $(NAME)',
339
+ });
273
340
  }
274
341
  }
275
342
 
@@ -288,27 +355,29 @@ function check( model ) { // = XSN
288
355
 
289
356
  for (const key of Object.keys(enumNode.enum)) {
290
357
  const element = enumNode.enum[key];
291
- if (!hasWrongType(element))
292
- continue;
293
-
294
- const actualType = element.value.literal;
295
- warning('enum-value-type', [ element.value.location, element ],
296
- { '#': expectedType, name: key, prop: actualType }, {
297
- std: 'Incorrect value type $(PROP) for enum element $(NAME)', // Not used
298
- number: 'Expected numeric value for enum element $(NAME) but was $(PROP)',
299
- string: 'Expected string value for enum element $(NAME) but was $(PROP)',
300
- });
358
+ if (hasWrongType(element)) {
359
+ const actualType = element.value.literal;
360
+ warning('type-unexpected-value', [ element.value.location, element ], {
361
+ '#': expectedType, name: key, prop: actualType,
362
+ }, {
363
+ std: 'Incorrect value type $(PROP) for enum element $(NAME)', // Not used
364
+ number: 'Expected numeric value for enum element $(NAME) but was $(PROP)',
365
+ string: 'Expected string value for enum element $(NAME) but was $(PROP)',
366
+ });
367
+ }
301
368
  }
302
369
  }
303
370
 
304
- // TODO: check inside compiler as it is a compiler restriction - improve
305
371
  /**
372
+ * TODO: check inside compiler as it is a compiler restriction - improve
373
+ * TODO: Recursive check; use "parentProps" as other member checks
374
+ *
306
375
  * Non-recursive check if sub-elements have a "localized" keyword since this is
307
376
  * not yet supported.
308
377
  *
309
378
  * This check is not recursive to avoid a runtime overhead. Because of this it fails
310
379
  * to detect scenarios with indirections, e.g.
311
- *
380
+ * ```cds
312
381
  * type L : localized String;
313
382
  * type L1 : L;
314
383
  * type L2 : L1;
@@ -318,6 +387,7 @@ function check( model ) { // = XSN
318
387
  * subElement : L2;
319
388
  * }
320
389
  * }
390
+ * ```
321
391
  *
322
392
  * @param {XSN.Artifact} element
323
393
  */
@@ -325,8 +395,8 @@ function check( model ) { // = XSN
325
395
  if (element._parent.kind !== 'element')
326
396
  return;
327
397
 
328
- const isLocalizedSubElement = element.localized && element.localized.val;
329
- if (isLocalizedSubElement || (element.type && isTypeLocalized(element.type._artifact))) {
398
+ const isLocalizedSubElement = element.localized?.val;
399
+ if (isLocalizedSubElement || element.type?._artifact?.localized?.val) {
330
400
  const loc = isLocalizedSubElement ? element.localized.location : element.type.location;
331
401
  warning('localized-sub-element', [ loc, element ],
332
402
  { type: element.type, '#': isLocalizedSubElement ? 'std' : 'type' },
@@ -335,61 +405,26 @@ function check( model ) { // = XSN
335
405
  type: 'Keyword "localized" in type $(TYPE) is ignored for sub elements',
336
406
  } );
337
407
  }
338
- return;
339
-
340
- // TODO: Recursive check
341
- function isTypeLocalized( type ) {
342
- return (type && type.localized && type.localized.val);
343
- }
344
408
  }
345
409
 
346
410
  /**
347
- * Check that a primary key element is not an unmanaged association or
348
- * contains unmanaged associations
411
+ * Check that min and max cardinalities of 'art' have legal values
349
412
  *
350
- * TODO: ease check for subelements: using unmanaged assocs is OK there, as
351
- * long as the whole key is "closed", i.e., no ref in ON refers to element
352
- * outside.
413
+ * TODO: move to define.js or parsers
353
414
  *
354
- * @param {any} element Element to check recursively
415
+ * @param {XSN.Artifact} art
355
416
  */
356
- function checkForUnmanagedAssociationsAsKey( element, parentProps ) {
357
- if (!parentProps.key?.val && !element.key?.val)
358
- return;
359
- if (element.targetAspect) {
360
- // TODO: bad location / message
361
- message('composition-as-key', [ parentProps.key.location, element ], {},
362
- // TODO: give semantics when error downgraded
363
- 'Managed compositions can\'t be used as primary key');
364
- }
365
- else if (element.on) {
366
- // TODO: bad location / message
367
- message('unmanaged-as-key', [ parentProps.key.location, element ], {},
368
- // TODO: give semantics when error downgraded
369
- 'Unmanaged associations can\'t be used as primary key');
370
- }
371
- }
372
-
373
- // Check that min and max cardinalities of 'elem' in 'art' have legal values
374
- // TODO: move to define.js or parsers
375
- function checkCardinality( elem ) {
376
- if (!elem.cardinality)
417
+ function checkCardinality( art ) {
418
+ if (!art.cardinality)
377
419
  return;
378
420
 
379
421
  // Max cardinalities must be a positive number or '*'
380
422
  for (const prop of [ 'sourceMax', 'targetMax' ]) {
381
- if (elem.cardinality[prop]) {
382
- const { literal, val, location } = elem.cardinality[prop];
383
- if (!(literal === 'number' && val > 0 ||
384
- literal === 'string' && val === '*')) {
385
- error('invalid-cardinality', [ location, elem ], { '#': prop, code: val, newcode: '*' }, {
386
- // eslint-disable-next-line max-len
387
- std: 'Value $(CODE) is invalid for maximum cardinality, expecting a positive number or $(NEWCODE)',
388
- // eslint-disable-next-line max-len
389
- sourceMax: 'Value $(CODE) is invalid for maximum source cardinality, expecting a positive number or $(NEWCODE)',
390
- // eslint-disable-next-line max-len
391
- targetMax: 'Value $(CODE) is invalid for maximum target cardinality, expecting a positive number or $(NEWCODE)',
392
- });
423
+ if (art.cardinality[prop]) {
424
+ const { literal, val, location } = art.cardinality[prop];
425
+ if (!(literal === 'number' && val > 0 || literal === 'string' && val === '*')) {
426
+ error( 'type-invalid-cardinality', [ location, art ],
427
+ { '#': prop, prop: val, otherprop: '*' } );
393
428
  }
394
429
  }
395
430
  }
@@ -398,34 +433,24 @@ function check( model ) { // = XSN
398
433
  // Note: Already checked by parser (syntax error if -1 is used) and
399
434
  // from-csn.json (expected non-negative number)
400
435
  for (const prop of [ 'sourceMin', 'targetMin' ]) {
401
- if (elem.cardinality[prop]) {
402
- const { literal, val, location } = elem.cardinality[prop];
403
- if (!(literal === 'number' && val >= 0)) {
404
- error('invalid-cardinality', [ location, elem ], { '#': prop, code: val }, {
405
- // eslint-disable-next-line max-len
406
- std: 'Value $(CODE) is invalid for minimum cardinality, expecting a non-negative number',
407
- // eslint-disable-next-line max-len
408
- targetMin: 'Value $(CODE) is invalid for minimum target cardinality, expecting a non-negative number',
409
- // eslint-disable-next-line max-len
410
- sourceMin: 'Value $(CODE) is invalid for minimum source cardinality, expecting a non-negative number',
411
- });
412
- }
436
+ if (art.cardinality[prop]) {
437
+ const { literal, val, location } = art.cardinality[prop];
438
+ if (!(literal === 'number' && val >= 0))
439
+ error( 'type-invalid-cardinality', [ location, art ], { '#': prop, prop: val } );
413
440
  }
414
441
  }
415
442
 
416
443
  // If provided, min cardinality must not exceed max cardinality (note that
417
444
  // '*' is considered to be >= any number)
418
- const pair = [ [ 'sourceMin', 'sourceMax', 'source' ], [ 'targetMin', 'targetMax', 'target' ] ];
419
- pair.forEach((p) => {
420
- if (elem.cardinality[p[0]] && elem.cardinality[p[1]] &&
421
- elem.cardinality[p[1]].literal === 'number' &&
422
- elem.cardinality[p[0]].val > elem.cardinality[p[1]].val) {
423
- error(null, [ elem.cardinality.location, elem ], { '#': p[2] }, {
424
- std: 'Minimum cardinality must not be greater than maximum cardinality', // variant unused
425
- source: 'Source minimum cardinality must not be greater than source maximum cardinality',
426
- target: 'Target minimum cardinality must not be greater than target maximum cardinality',
427
- });
428
- }
445
+ const pair = [
446
+ [ 'sourceMin', 'sourceMax', 'sourceVal' ],
447
+ [ 'targetMin', 'targetMax', 'targetVal' ],
448
+ ];
449
+ pair.forEach(([ lhs, rhs, variant ]) => {
450
+ if (art.cardinality[lhs] && art.cardinality[rhs] &&
451
+ art.cardinality[rhs].literal === 'number' &&
452
+ art.cardinality[lhs].val > art.cardinality[rhs].val)
453
+ error( 'type-invalid-cardinality', [ art.cardinality.location, art ], { '#': variant } );
429
454
  });
430
455
  }
431
456
 
@@ -452,10 +477,14 @@ function check( model ) { // = XSN
452
477
  }
453
478
 
454
479
  function checkAssociation( elem ) {
480
+ if (!elem.target && !elem.targetAspect)
481
+ return;
455
482
  // TODO: yes, a check similar to this could make it into the compiler)
456
483
  // when virtual element is part of association
484
+ let fkCount = 0;
457
485
  if (elem.foreignKeys) {
458
486
  for (const k in elem.foreignKeys) {
487
+ ++fkCount;
459
488
  const key = elem.foreignKeys[k].targetElement;
460
489
  if (key && isVirtualElement(key._artifact))
461
490
  error('ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' });
@@ -463,6 +492,30 @@ function check( model ) { // = XSN
463
492
  error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );
464
493
  }
465
494
  }
495
+ if (elem.default) {
496
+ if (!isBetaEnabled( model.options, 'associationDefault' )) {
497
+ error( 'type-unsupported-default', [ elem.default.location, elem ], {
498
+ '#': isComposition( model, elem ) ? 'comp' : 'std',
499
+ }, {
500
+ std: 'Unsupported default value on an association',
501
+ comp: 'Unsupported default value on a composition',
502
+ } );
503
+ }
504
+ else if (elem.targetAspect || elem.on || fkCount !== 1) {
505
+ const variant = (elem.targetAspect && 'targetAspect') || (elem.on && 'onCond') || 'multi';
506
+ error( 'type-unexpected-default', [ elem.default.location, elem ], {
507
+ '#': variant, keyword: 'default', count: fkCount,
508
+ } );
509
+ }
510
+ else {
511
+ const fkName = Object.keys(elem.foreignKeys)[0];
512
+ if (elem.foreignKeys[fkName].targetElement._artifact?._effectiveType?.elements) {
513
+ error( 'type-unexpected-default', [ elem.default.location, elem ], {
514
+ '#': 'structured', keyword: 'default', name: fkName,
515
+ } );
516
+ }
517
+ }
518
+ }
466
519
 
467
520
  checkOnCondition(elem);
468
521
  }
@@ -473,8 +526,9 @@ function check( model ) { // = XSN
473
526
  args[1] || op;
474
527
  }
475
528
 
476
- // A function like this could be part of the compiler
477
529
  /**
530
+ * TODO: A function like this could be part of the compiler
531
+ *
478
532
  * Check that the given type has no conflicts between its `type` property
479
533
  * and its `elements` or `items` property. For example if `type` is not
480
534
  * structured but the artifact has an `elements` property then the user
@@ -485,7 +539,7 @@ function check( model ) { // = XSN
485
539
  function checkTypeStructure( artifact ) {
486
540
  // Just a basic check. We do not check that the inner structure of `items`
487
541
  // is the same as the type but only that all are arrayed or structured.
488
- if (artifact.type && artifact.type._artifact) {
542
+ if (artifact.type?._artifact) {
489
543
  const finalType = artifact.type._artifact._effectiveType || artifact.type._artifact;
490
544
 
491
545
  if (artifact.items && !finalType.items) {
@@ -553,7 +607,7 @@ function check( model ) { // = XSN
553
607
  if (elem === undefined) {
554
608
  const loc = [ elements[$location], user ];
555
609
  error('ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element });
556
- return false; // only report one
610
+ return false; // only report once
557
611
  }
558
612
  else if (!checkElementOverride(elem, orig)) {
559
613
  return false;
@@ -564,8 +618,6 @@ function check( model ) { // = XSN
564
618
  }
565
619
 
566
620
 
567
- // Former checkExpressions.js ----------------------------------------------
568
-
569
621
  /**
570
622
  * Check a generic expression (or condition) for semantic validity.
571
623
  *
@@ -576,6 +628,10 @@ function check( model ) { // = XSN
576
628
  function checkGenericExpression( xpr, user ) {
577
629
  checkExpressionNotVirtual(xpr, user);
578
630
  checkExpressionAssociationUsage(xpr, user, false);
631
+ if (xpr.op?.val === 'cast') {
632
+ requireExplicitTypeInSqlCast( xpr, user );
633
+ checkTypeArguments( xpr, user );
634
+ }
579
635
  }
580
636
 
581
637
  function checkExpressionNotVirtual( xpr, user ) {
@@ -589,6 +645,8 @@ function check( model ) { // = XSN
589
645
  visitExpression(elem.on, elem, (xpr, user) => {
590
646
  checkExpressionNotVirtual(xpr, user);
591
647
  checkExpressionAssociationUsage(xpr, user, true);
648
+ // Essential check. Dependency handling for `on` conditions must change if
649
+ // this is allowed. See test3/Associations/Dependencies/.
592
650
  if (xpr._artifact?.$syntax === 'calc' && !xpr._artifact.value.stored?.val)
593
651
  error( 'ref-unexpected-calculated', [ xpr.location, user ], { '#': 'on' } );
594
652
  });
@@ -600,11 +658,16 @@ function check( model ) { // = XSN
600
658
  }
601
659
 
602
660
  function checkSelectItemValue( elem ) {
603
- checkExpressionAssociationUsage(elem.value, elem, false);
604
-
661
+ checkExpressionAssociationUsage( elem.value, elem, false );
662
+ // If a direct SQL-style cast() has no type, but type props, the compiler does not copy the type
663
+ // props/does not use the cast(). To avoid duplicate messages, only run this check if there is
664
+ // no explicit type, as otherwise we will check the cast() twice (once here, once via element).
665
+ if (elem.value?.op?.val === 'cast' && !elem.type) {
666
+ requireExplicitTypeInSqlCast( elem.value, elem );
667
+ checkTypeArguments( elem.value, elem );
668
+ }
605
669
  visitSubExpression(elem.value, elem, (xpr) => {
606
- checkExpressionNotVirtual(xpr, elem);
607
- checkExpressionAssociationUsage(xpr, elem, false);
670
+ checkGenericExpression( xpr, elem );
608
671
  });
609
672
  }
610
673
 
@@ -615,12 +678,16 @@ function check( model ) { // = XSN
615
678
  checkExpressionNotVirtual(xpr, user);
616
679
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
617
680
  // And users can't change structured to non-structured elements.
618
- if (!elem.$inferred && isStructuredElement(xpr._artifact))
681
+ if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
619
682
  error('ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
620
- else if (xpr._artifact.target !== undefined)
621
- error('ref-unexpected-assoc', [ sourceLoc, elem ], { '#': 'expr' });
622
- else if (xpr._artifact.localized?.val)
683
+ }
684
+ else if (xpr._artifact.target !== undefined) {
685
+ const variant = isComposition(model, xpr._artifact) ? 'expr-comp' : 'expr';
686
+ error('ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant });
687
+ }
688
+ else if (xpr._artifact.localized?.val && elem.value.stored?.val) {
623
689
  error('ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' });
690
+ }
624
691
  }
625
692
  });
626
693
  // Calculated elements must not refer to keys, because that may lead to another
@@ -676,13 +743,6 @@ function check( model ) { // = XSN
676
743
  return false;
677
744
  }
678
745
 
679
- function isStructuredElement( elem ) {
680
- // The effective type always points to something with elements _if_ the
681
- // type is structured. But `elem` should already have `elements` if its
682
- // structured due to element expansion.
683
- return !!(elem?._effectiveType || elem)?.elements;
684
- }
685
-
686
746
  /**
687
747
  * Check a tree-like expression for semantic validity
688
748
  *
@@ -721,11 +781,14 @@ function check( model ) { // = XSN
721
781
  // Arg must not be an association and not $self
722
782
  // Only if path is not approved exists path (that is non-query position)
723
783
  if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
724
- if (arg.$expected === 'exists')
725
- error('ref-unexpected-assoc', [ arg.location, user ], { '#': 'expr' } );
784
+ if (arg.$expected === 'exists') {
785
+ const variant = isComposition(model, arg._artifact) ? 'expr-comp' : 'expr';
786
+ error('ref-unexpected-assoc', [ arg.location, user ], { '#': variant });
787
+ }
726
788
  }
727
789
  else if (!allowAssocTail && isAssociationOperand(arg)) {
728
- error('ref-unexpected-assoc', [ arg.location, user ], { '#': 'expr' } );
790
+ const variant = isComposition(model, arg._artifact) ? 'expr-comp' : 'expr';
791
+ error('ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
729
792
  }
730
793
 
731
794
  if (isDollarSelfOrProjectionOperand(arg)) {
@@ -734,15 +797,12 @@ function check( model ) { // = XSN
734
797
  }
735
798
  }
736
799
 
737
- // Return true if 'arg' is an expression argument of type association or composition
800
+ /**
801
+ * Return true if 'arg' is an expression argument of type association or composition.
802
+ */
738
803
  function isAssociationOperand( arg ) {
739
- if (!arg.path) {
740
- // Not a path, hence not an association (literal, expression, function, whatever ...)
741
- return false;
742
- }
743
804
  // If it has a target, it is an association or composition
744
- return (arg._artifact && arg._artifact.target) ||
745
- (arg._artifact && arg._artifact._effectiveType && arg._artifact._effectiveType.target);
805
+ return !!arg._artifact?._effectiveType?.target;
746
806
  }
747
807
 
748
808
  /**
@@ -764,7 +824,6 @@ function check( model ) { // = XSN
764
824
  if (!xpr.op || !xpr.args)
765
825
  return false;
766
826
 
767
-
768
827
  // One argument must be "$self" and the other an assoc
769
828
  if (xpr.op.val === '=' && xpr.args.length === 2) {
770
829
  // Tree-ish expression from the compiler (not augmented)
@@ -787,13 +846,13 @@ function check( model ) { // = XSN
787
846
  // Check the annotation assignments (if any) of 'annotatable', possibly using annotation
788
847
  // definitions from 'model'. Report errors on 'options.messages.
789
848
  //
790
- // TODO: rework completely
849
+ // TODO: rework completely!
791
850
 
792
851
  // Has been slightly adapted for model.vocabularies but comments need to be
793
852
  // adapted, etc.
794
853
  function checkAnnotationAssignment1( art, anno ) {
795
854
  // Sanity checks (ignore broken assignments)
796
- if (!anno.name || !anno.name.path || !anno.name.path.length)
855
+ if (!anno.name?.path?.length)
797
856
  return;
798
857
  // Annotation artifact for longest path step of annotation path
799
858
  let fromArtifact = null;
@@ -827,7 +886,6 @@ function check( model ) { // = XSN
827
886
  if (!annoDecl || annoDecl.artifacts && !elementDecl)
828
887
  return;
829
888
 
830
-
831
889
  // Must be an annotation if found
832
890
  if (annoDecl.kind !== 'annotation') // i.e namespace
833
891
  return;
@@ -847,7 +905,7 @@ function check( model ) { // = XSN
847
905
 
848
906
  // Must have literal or path unless it is a boolean
849
907
  if (!anno.literal && !anno.path && getFinalTypeNameOf(elementDecl) !== 'cds.Boolean') {
850
- if (elementDecl.type && elementDecl.type._artifact.name.absolute) {
908
+ if (elementDecl.type?._artifact.name.absolute) {
851
909
  warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
852
910
  { '#': 'type', type: elementDecl.type._artifact });
853
911
  }
@@ -868,7 +926,7 @@ function check( model ) { // = XSN
868
926
  // if not
869
927
  function checkValueAssignableTo( annoDef, value, elementDecl, art ) {
870
928
  // FIXME: We currently do not have any element declaration that could match
871
- // a 'path' value, so we simply leave those alone
929
+ // a 'path' value, so we simply leave those alone
872
930
  if (value.path)
873
931
  return;
874
932
 
@@ -900,6 +958,7 @@ function check( model ) { // = XSN
900
958
  }
901
959
 
902
960
  // Handle each (primitive) expected element type separately
961
+ // TODO: Don't rely on name; use actual type
903
962
  const type = getFinalTypeNameOf(elementDecl);
904
963
  if (builtins.isStringTypeName(type)) {
905
964
  if (value.literal !== 'string' && value.literal !== 'enum' &&
@@ -978,8 +1037,7 @@ function check( model ) { // = XSN
978
1037
  return null;
979
1038
  }
980
1039
 
981
- // TODO: remove the following
982
-
1040
+ // TODO: remove
983
1041
  // Return the artifact (and possibly, its element) found by following 'path'
984
1042
  // starting at 'from'. The return value is an object { artifact, endOfPath }
985
1043
  // with 'artifact' being the last artifact encountered on 'path' (or
@@ -1004,6 +1062,7 @@ function check( model ) { // = XSN
1004
1062
  return resolvePathFrom(path.slice(1), nextStepEnv[path[0].id], result);
1005
1063
  }
1006
1064
 
1065
+ // TODO: remove
1007
1066
  // Return the absolute name of the final type of 'node'. May return 'undefined'
1008
1067
  // for anonymous types. DO NOT USE THIS function, it has several assumptions
1009
1068
  // which are not necessarily true.
@@ -1016,7 +1075,7 @@ function check( model ) { // = XSN
1016
1075
  }
1017
1076
 
1018
1077
  /**
1019
- * Ensure that the `locale` element of sap.common.TextsAspects
1078
+ * Ensure that the `locale` element of `sap.common.TextsAspects`
1020
1079
  * is a string type. This is required by CAP runtimes to work properly.
1021
1080
  *
1022
1081
  * @param {XSN.Model} model
@@ -1026,11 +1085,10 @@ function checkSapCommonTextsAspects( model ) {
1026
1085
  const locale = model.definitions[name]?.elements?.locale;
1027
1086
  if (locale) {
1028
1087
  // `locale` could also be `sap.common.Locale`, which must also be a string.
1029
- const type = locale._effectiveType;
1030
- if (type?.name?.absolute !== 'cds.String') {
1088
+ if (locale._effectiveType !== model.definitions['cds.String']) {
1031
1089
  const hasCommonLocale = !!model.definitions['sap.common.Locale'];
1032
1090
  const { error } = model.$messageFunctions;
1033
- error('def-invalid-element-type', [ locale.type.location, locale ], {
1091
+ error( 'def-invalid-element-type', [ locale.type.location, locale ], {
1034
1092
  '#': hasCommonLocale ? 'texts-aspect-locale' : 'std',
1035
1093
  art: name,
1036
1094
  elemref: 'locale',
@@ -1050,12 +1108,11 @@ function checkSapCommonTextsAspects( model ) {
1050
1108
  function checkSapCommonLocale( model ) {
1051
1109
  const localeArt = model.definitions['sap.common.Locale'];
1052
1110
  if (localeArt) {
1053
- const type = localeArt._effectiveType;
1054
- if (type?.name?.absolute !== 'cds.String') {
1111
+ if (localeArt._effectiveType !== model.definitions['cds.String']) {
1055
1112
  const { message } = model.$messageFunctions;
1056
- message('type-expected-builtin', [ localeArt.name.location, localeArt ],
1057
- { name: 'sap.common.Locale' },
1058
- 'Expected $(NAME) to be a string type');
1113
+ message( 'type-expected-builtin', [ localeArt.name.location, localeArt ],
1114
+ { name: 'sap.common.Locale' },
1115
+ 'Expected $(NAME) to be a string type' );
1059
1116
  }
1060
1117
  }
1061
1118
  }
@@ -1065,7 +1122,7 @@ function checkSapCommonLocale( model ) {
1065
1122
  * Visits each expression.
1066
1123
  *
1067
1124
  * TODO: Properly visit expressions; will be improved step by step;
1068
- * Currently only replaces old foreachPath().
1125
+ * Currently only replaces old foreachPath(), which had very poor performance.
1069
1126
  *
1070
1127
  * @param {any} xpr
1071
1128
  * @param {XSN.Artifact} user
@@ -1104,5 +1161,25 @@ function visitSubExpression( xpr, user, callback ) {
1104
1161
  }
1105
1162
  }
1106
1163
 
1164
+ /**
1165
+ * Whether the given element is a composition.
1166
+ * TODO: `type T: Composition of E; entity V { e: T default 3 };`
1167
+ * See also getUnderlyingBuiltinType()/compositionTextVariant() in utils.js.
1168
+ *
1169
+ * @return {boolean}
1170
+ */
1171
+ function isComposition( model, elem ) {
1172
+ elem = elem?._effectiveType;
1173
+ if (!elem || !elem.target)
1174
+ return false;
1175
+ do {
1176
+ if (elem.type?._artifact === model.definitions['cds.Composition'])
1177
+ return true;
1178
+ // Because inferred elements don't have a direct `type` property,
1179
+ // we need to go along the origin chain.
1180
+ elem = elem._origin;
1181
+ } while (elem);
1182
+ return false;
1183
+ }
1107
1184
 
1108
1185
  module.exports = check;