@sap/cds-compiler 4.0.2 → 4.2.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 (101) hide show
  1. package/CHANGELOG.md +200 -5
  2. package/bin/cdsc.js +18 -15
  3. package/doc/CHANGELOG_BETA.md +16 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +33 -13
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +25 -25
  8. package/lib/base/location.js +6 -7
  9. package/lib/base/message-registry.js +123 -42
  10. package/lib/base/messages.js +18 -10
  11. package/lib/base/model.js +43 -10
  12. package/lib/checks/defaultValues.js +6 -6
  13. package/lib/checks/elements.js +11 -10
  14. package/lib/checks/foreignKeys.js +0 -5
  15. package/lib/checks/manyNavigations.js +33 -0
  16. package/lib/checks/onConditions.js +22 -14
  17. package/lib/checks/queryNoDbArtifacts.js +132 -73
  18. package/lib/checks/selectItems.js +4 -55
  19. package/lib/checks/sql-snippets.js +15 -4
  20. package/lib/checks/types.js +3 -3
  21. package/lib/checks/utils.js +4 -3
  22. package/lib/checks/validator.js +3 -1
  23. package/lib/compiler/.eslintrc.json +2 -1
  24. package/lib/compiler/assert-consistency.js +71 -40
  25. package/lib/compiler/base.js +7 -2
  26. package/lib/compiler/builtins.js +40 -41
  27. package/lib/compiler/checks.js +415 -367
  28. package/lib/compiler/classes.js +62 -0
  29. package/lib/compiler/cycle-detector.js +9 -9
  30. package/lib/compiler/define.js +124 -90
  31. package/lib/compiler/extend.js +115 -88
  32. package/lib/compiler/finalize-parse-cdl.js +26 -25
  33. package/lib/compiler/generate.js +57 -49
  34. package/lib/compiler/index.js +56 -56
  35. package/lib/compiler/kick-start.js +10 -7
  36. package/lib/compiler/moduleLayers.js +1 -1
  37. package/lib/compiler/populate.js +180 -144
  38. package/lib/compiler/propagator.js +10 -9
  39. package/lib/compiler/resolve.js +321 -246
  40. package/lib/compiler/shared.js +812 -433
  41. package/lib/compiler/tweak-assocs.js +114 -50
  42. package/lib/compiler/utils.js +241 -46
  43. package/lib/edm/.eslintrc.json +40 -1
  44. package/lib/edm/annotations/genericTranslation.js +721 -707
  45. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  46. package/lib/edm/csn2edm.js +389 -378
  47. package/lib/edm/edm.js +679 -770
  48. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  49. package/lib/edm/edmInboundChecks.js +29 -27
  50. package/lib/edm/edmPreprocessor.js +689 -648
  51. package/lib/edm/edmUtils.js +279 -300
  52. package/lib/gen/Dictionary.json +34 -10
  53. package/lib/gen/language.checksum +1 -1
  54. package/lib/gen/language.interp +1 -1
  55. package/lib/gen/languageParser.js +2857 -2856
  56. package/lib/json/from-csn.js +77 -51
  57. package/lib/json/to-csn.js +15 -15
  58. package/lib/language/antlrParser.js +2 -1
  59. package/lib/language/genericAntlrParser.js +52 -43
  60. package/lib/language/language.g4 +61 -64
  61. package/lib/language/multiLineStringParser.js +2 -0
  62. package/lib/main.d.ts +65 -0
  63. package/lib/model/csnRefs.js +37 -19
  64. package/lib/model/csnUtils.js +51 -18
  65. package/lib/model/revealInternalProperties.js +30 -22
  66. package/lib/modelCompare/compare.js +149 -41
  67. package/lib/modelCompare/utils/filter.js +55 -25
  68. package/lib/optionProcessor.js +21 -9
  69. package/lib/render/manageConstraints.js +20 -17
  70. package/lib/render/toCdl.js +63 -23
  71. package/lib/render/toHdbcds.js +2 -2
  72. package/lib/render/toRename.js +4 -9
  73. package/lib/render/toSql.js +82 -35
  74. package/lib/render/utils/common.js +11 -9
  75. package/lib/render/utils/unique.js +52 -0
  76. package/lib/transform/db/applyTransformations.js +62 -21
  77. package/lib/transform/db/assertUnique.js +7 -8
  78. package/lib/transform/db/associations.js +2 -2
  79. package/lib/transform/db/cdsPersistence.js +9 -9
  80. package/lib/transform/db/constraints.js +47 -17
  81. package/lib/transform/db/expansion.js +138 -68
  82. package/lib/transform/db/flattening.js +98 -30
  83. package/lib/transform/db/rewriteCalculatedElements.js +20 -14
  84. package/lib/transform/db/temporal.js +1 -1
  85. package/lib/transform/db/transformExists.js +8 -7
  86. package/lib/transform/db/views.js +73 -33
  87. package/lib/transform/draft/db.js +11 -9
  88. package/lib/transform/draft/odata.js +1 -1
  89. package/lib/transform/{forOdataNew.js → forOdata.js} +10 -7
  90. package/lib/transform/forRelationalDB.js +148 -136
  91. package/lib/transform/localized.js +92 -54
  92. package/lib/transform/odata/toFinalBaseType.js +3 -3
  93. package/lib/transform/{transformUtilsNew.js → transformUtils.js} +13 -111
  94. package/lib/transform/translateAssocsToJoins.js +14 -28
  95. package/lib/utils/file.js +7 -7
  96. package/lib/utils/moduleResolve.js +210 -121
  97. package/lib/utils/objectUtils.js +1 -1
  98. package/package.json +5 -5
  99. package/share/messages/check-proper-type-of.md +1 -1
  100. package/share/messages/{check-proper-type.md → def-missing-type.md} +3 -5
  101. package/share/messages/message-explanations.json +1 -1
@@ -1,33 +1,37 @@
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
+ forEachMemberRecursively,
19
+ isBetaEnabled,
22
20
  } = require('../base/model');
23
21
  const { CompilerAssertion } = require('../base/error');
24
22
  const { pathName } = require('./utils');
25
- const { forEachMemberRecursively } = require('../model/csnUtils');
26
- const $location = Symbol.for('cds.$location');
23
+ const { typeParameters } = require('./builtins');
27
24
 
28
- function check( model ) { // = XSN
25
+ const $location = Symbol.for( 'cds.$location' );
26
+
27
+ /**
28
+ * Run compiler checks on the given XSN model.
29
+ *
30
+ * @param {XSN.Model} model
31
+ */
32
+ function check( model ) {
29
33
  const {
30
- error, warning, message, info,
34
+ error, warning, info,
31
35
  } = model.$messageFunctions;
32
36
 
33
37
  checkSapCommonLocale( model );
@@ -35,34 +39,37 @@ function check( model ) { // = XSN
35
39
 
36
40
  forEachDefinition( model, checkDefinition );
37
41
  forEachGeneric( model, 'vocabularies', checkAnnotationDefinition );
42
+
38
43
  return;
39
44
 
40
45
  function checkDefinition( def ) {
41
46
  checkGenericConstruct( def );
42
47
  if (def.includes && def.elements)
43
48
  checkElementIncludeOverride( def );
44
- forEachMember( def, member => checkMember(member) );
49
+ forEachMember( def, member => checkMember( member ) );
45
50
  if (def.$queries)
46
51
  def.$queries.forEach( checkQuery );
47
52
  }
48
53
 
49
54
  function checkAnnotationDefinition( art ) {
55
+ // TODO: Should we check elements similar to definition-elements as well?
50
56
  checkEnumType( art );
51
57
  forEachMemberRecursively( art, (member) => {
52
58
  if (member.localized?.val)
53
59
  warning( 'def-unexpected-localized-anno', [ member.localized.location, member ] );
54
- });
55
- // TODO: Should we check elements similar to definition-elements as well?
60
+ } );
56
61
  }
57
62
 
58
63
  function checkGenericConstruct( art ) {
59
64
  checkName( art );
65
+ checkTypeArguments( art );
60
66
  if (model.vocabularies) {
61
67
  Object.keys( art )
62
- .filter( a => a.startsWith('@') )
68
+ .filter( a => a.startsWith( '@' ) )
63
69
  .forEach( a => checkAnnotationAssignment1( art, art[a] ) );
64
70
  }
65
- checkTypeStructure(art);
71
+ checkTypeStructure( art );
72
+ checkAssociation( art ); // type def could be assoc
66
73
  if (art.kind === 'enum')
67
74
  checkEnum( art );
68
75
  checkEnumType( art );
@@ -75,106 +82,177 @@ function check( model ) { // = XSN
75
82
  if (member.virtual?.val === true)
76
83
  parentProps.virtual = member.virtual;
77
84
 
78
- checkGenericConstruct(member);
85
+ checkGenericConstruct( member );
79
86
 
80
87
  if (member.kind === 'element')
81
88
  checkElement( member, parentProps );
82
89
 
83
- forEachMember( member, m => checkMember(m, parentProps) );
90
+ forEachMember( member, m => checkMember( m, parentProps ) );
84
91
  }
85
92
 
86
93
  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);
94
+ const isKey = parentProps.key?.val || elem.key?.val;
95
+ const isVirtual = parentProps.virtual?.val || elem.virtual?.val;
89
96
  if (isKey && isVirtual) {
90
- error('def-unexpected-key', [ isKey.location, elem ],
91
- { '#': 'virtual', name: elem.name.element, prop: 'key' });
97
+ error( 'def-unexpected-key', [ isKey.location, elem ],
98
+ { '#': 'virtual', art: elem.name.element, prop: 'key' } );
92
99
  }
93
100
  }
94
101
 
95
102
  function checkElement( elem, parentProps ) {
96
- checkLocalizedSubElement(elem);
97
- checkVirtualKey(elem, parentProps);
98
- checkForUnmanagedAssociationsAsKey( elem, parentProps );
103
+ checkLocalizedSubElement( elem );
104
+ checkVirtualKey( elem, parentProps );
99
105
  checkLocalizedElement( elem );
100
- checkAssociation( elem );
101
106
 
102
107
  if (elem.value) {
103
108
  if (elem._main.query)
104
- checkSelectItemValue(elem);
109
+ checkSelectItemValue( elem );
105
110
  else if (elem.$syntax === 'calc')
106
111
  checkCalculatedElementValue( elem );
107
112
  }
108
113
 
109
- checkCardinality(elem); // TODO: also for assoc types
114
+ checkCardinality( elem ); // TODO: also for assoc types
110
115
  }
111
116
 
112
117
 
113
118
  function checkName( construct ) { // TODO: move to define.js
114
119
  if (model.options.$skipNameCheck)
115
120
  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
120
- error(null, [ construct.name.location, construct ], {},
121
- 'The character \'.\' is not allowed in identifiers');
121
+ // TODO: Move a corrected version of this check to definer (but do not rely on it!):
122
+ // The code below misses to consider CSN input!
123
+ // Maybe remove the check? But consider runtimes that rely on '.' as element separator.
124
+ if (construct.kind === 'element' || construct.kind === 'action' || construct.kind === 'param') {
125
+ if (construct.name.id?.includes( '.' )) {
126
+ error( null, [ construct.name.location, construct ], {},
127
+ 'The character \'.\' is not allowed in identifiers' );
128
+ }
129
+ }
130
+ }
131
+
132
+
133
+ /**
134
+ * Check the type arguments on `art`, e.g. cds.Decimal can't have a `length`, structures
135
+ * can't have `precision`, etc.
136
+ *
137
+ * @param {XSN.Artifact} art
138
+ * @param {XSN.Artifact} user
139
+ */
140
+ function checkTypeArguments( art, user = art ) {
141
+ if (art.builtin || art.kind === 'context' || art.kind === 'service')
142
+ return;
143
+
144
+ if (art.items)
145
+ checkTypeArguments( art.items, art );
146
+
147
+ const actualParams = typeParameters.list.filter( param => art[param] !== undefined );
148
+ if (actualParams.length === 0)
149
+ return;
150
+
151
+ const typeArt = art.type?._artifact || art;
152
+
153
+ // Note: `_effectiveType` points to `art` itself, if it is an enum type,
154
+ // descend to the origin in this case.
155
+ let effectiveType = typeArt._effectiveType;
156
+ while (effectiveType?.enum)
157
+ effectiveType = (effectiveType._origin || effectiveType.type?._artifact)?._effectiveType;
158
+
159
+ if (!effectiveType) {
160
+ return; // e.g. illegal definition references, cycles, ...
161
+ }
162
+ else if (!art.type && !effectiveType.type && !effectiveType?.builtin) {
163
+ error( 'type-missing-type', [ art.location, user ],
164
+ { otherprop: 'type', prop: actualParams[0] },
165
+ 'Missing $(OTHERPROP) property next to $(PROP)' );
166
+ return;
167
+ }
168
+
169
+ const expectedParams = effectiveType.parameters &&
170
+ effectiveType.parameters.map( p => p.name || p ) || [];
171
+
172
+ for (const param of actualParams) {
173
+ if (!expectedParams.includes( param )) {
174
+ // Whether the type ref itself is a builtin or a custom type with a builtin as base.
175
+ let variant;
176
+ if ((art.type?._artifact || art._effectiveType).builtin)
177
+ variant = 'builtin';
178
+ else if (effectiveType.builtin)
179
+ variant = 'type';
180
+ else // effectiveType is not a builtin -> array or structured
181
+ variant = 'non-scalar';
182
+
183
+ error( 'type-unexpected-argument', [ art[param].location, user ], {
184
+ '#': variant, prop: param, art: art.type || art._effectiveType, type: effectiveType,
185
+ } );
186
+ break; // Avoid spam: Only emit the first error.
187
+ }
188
+ else if (!typeParameters.expectedLiteralsFor[param].includes( art[param].literal )) {
189
+ error( 'type-unexpected-argument', [ art[param].location, user ], {
190
+ '#': 'incorrect-type',
191
+ prop: param,
192
+ code: art[param].literal,
193
+ names: typeParameters.expectedLiteralsFor[param],
194
+ } );
195
+ break; // Avoid spam: Only emit the first error.
196
+ }
197
+ }
198
+ }
199
+
200
+ function requireExplicitTypeInSqlCast( art, user ) {
201
+ if (!art.type) {
202
+ error( 'expr-missing-type', [ art.location, user ], { },
203
+ 'Missing type in SQL cast function' );
122
204
  }
123
205
  }
124
206
 
125
207
  function checkLocalizedElement( elem ) {
126
- // if it is directly a localized element
127
- if (elem.localized && elem.localized.val) {
208
+ if (elem.localized?.val) {
128
209
  const type = elem._effectiveType;
129
210
  // See discussion issue #6520: should we allow all scalar types?
130
211
  if (!type || !type.builtin || type.category !== 'string') {
131
- info('ref-expecting-localized-string', [ elem.type?.location, elem ],
132
- { keyword: 'localized' },
133
- 'Expecting a string type in combination with keyword $(KEYWORD)');
212
+ info( 'ref-expecting-localized-string', [ elem.type?.location, elem ],
213
+ { keyword: 'localized' },
214
+ 'Expecting a string type in combination with keyword $(KEYWORD)' );
134
215
  }
135
216
  }
136
- // "key" keyword at localized element in SELECT list.
217
+
137
218
  // TODO: This check should be moved to localized.js
138
- if (elem.key && elem.key.val && elem._main && elem._main.query) {
219
+ // "key" keyword at localized element in SELECT list.
220
+ if (elem.key?.val && elem._main?.query) {
139
221
  // 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' },
144
- 'Keyword $(KEYWORD) is ignored for primary keys');
222
+ // already resulted in a warning by localized.js
223
+ if (elem._origin?.localized?.val && !elem._origin.key?.val) {
224
+ warning( 'def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' },
225
+ 'Keyword $(KEYWORD) is ignored for primary keys' );
145
226
  }
146
227
  }
147
228
  }
148
229
 
149
230
  function checkQuery( query ) {
150
- checkNoUnmanagedAssocsInGroupByOrderBy( query );
151
231
  // TODO: check too simple (just one source), as most of those in this file
152
232
  // Check expressions in the various places where they may occur
153
233
  if (query.from)
154
- visitSubExpression(query.from, query, checkGenericExpression);
234
+ visitSubExpression( query.from, query, checkGenericExpression );
155
235
 
156
236
  if (query.where)
157
- visitExpression(query.where, query, checkGenericExpression);
237
+ visitExpression( query.where, query, checkGenericExpression );
158
238
 
159
239
  if (query.groupBy) {
160
240
  for (const groupByEntry of query.groupBy)
161
- visitExpression(groupByEntry, query, checkGenericExpression);
241
+ visitExpression( groupByEntry, query, checkGenericExpression );
162
242
  }
163
243
  if (query.having)
164
- visitExpression(query.having, query, checkGenericExpression);
244
+ visitExpression( query.having, query, checkGenericExpression );
165
245
 
166
246
  if (query.orderBy) {
167
247
  for (const orderByEntry of query.orderBy)
168
- visitExpression(orderByEntry, query, checkGenericExpression);
248
+ visitExpression( orderByEntry, query, checkGenericExpression );
169
249
  }
170
250
  if (query.mixin) {
171
251
  for (const mixinName in query.mixin)
172
- checkAssociation(query.mixin[mixinName]);
252
+ checkAssociation( query.mixin[mixinName] );
173
253
  }
174
254
  }
175
255
 
176
- // Individual checks -------------------------------------------------------
177
-
178
256
  /**
179
257
  * The enumNode is a single enum element and not the whole type.
180
258
  *
@@ -190,8 +268,8 @@ function check( model ) { // = XSN
190
268
  // Special handling to print a more detailed error message.
191
269
  // Other cases like `null` as enum value are handled in `checkEnumValueType()`
192
270
  if (type === 'enum') {
193
- warning('enum-value-ref', [ loc, enumNode ], {}, // TODO: make this an error in v4
194
- 'References to other values are not allowed as enum values');
271
+ warning( 'ref-unexpected-enum', [ loc, enumNode ], {},
272
+ 'References to other values are not allowed as enum values' );
195
273
  }
196
274
  }
197
275
 
@@ -201,8 +279,7 @@ function check( model ) { // = XSN
201
279
  enumNode = enumNode.enum ? enumNode : enumNode.items;
202
280
  if (!enumNode || !enumNode.enum)
203
281
  return;
204
- const type = enumNode.type && enumNode.type._artifact &&
205
- enumNode.type._artifact._effectiveType;
282
+ const type = enumNode?.type?._artifact?._effectiveType;
206
283
 
207
284
  // We can't distinguish (in CSN) between these two cases:
208
285
  // type Base : String enum { b;a = 'abc'; };
@@ -212,64 +289,57 @@ function check( model ) { // = XSN
212
289
  if (!type || type.enum)
213
290
  return;
214
291
 
215
- const name = type.name.absolute;
216
-
217
292
  // All builtin types are allowed except binary and relational types.
218
293
  // 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';
294
+ if (!type.builtin || type.internal || type.category === 'binary') {
295
+ let typeClass = 'std';
296
+ if (type.category === 'binary' || type.category === 'relation')
297
+ typeClass = type.category;
228
298
  else if (type.elements)
229
- typeclass = 'struct';
299
+ typeClass = 'struct';
230
300
  else if (type.items)
231
- typeclass = 'items';
301
+ typeClass = 'items';
232
302
 
233
- error('enum-invalid-type', [ enumNode.type.location, enumNode ], { '#': typeclass }, {
303
+ error( 'type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, {
234
304
  std: 'Only builtin types are allowed as enums',
235
305
  binary: 'Binary types are not allowed as enums',
236
306
  relation: 'Relational types are not allowed as enums',
237
307
  struct: 'Structured types are not allowed as enums',
238
308
  items: 'Arrayed types are not allowed as enums',
239
- });
309
+ } );
240
310
  return;
241
311
  }
242
312
 
243
- checkEnumValue(enumNode);
313
+ checkEnumValue( enumNode );
244
314
  }
245
315
 
246
316
  /**
247
- * Check the given enum's elements and their values. For example
317
+ * Check the given enum's elements and their values. For example,
248
318
  * whether the value types are valid for the used enum type.
249
319
  * `enumNode` can be also be `type.items` if the type is an arrayed enum.
250
320
  *
251
321
  * @param {XSN.Definition} enumNode
252
322
  */
253
323
  function checkEnumValue( enumNode ) {
254
- const type = enumNode.type && enumNode.type._artifact &&
255
- enumNode.type._artifact._effectiveType;
256
- if (!enumNode.enum || !type || !type.builtin)
324
+ const type = enumNode.type?._artifact?._effectiveType;
325
+ if (!type || !enumNode.enum || !type.builtin)
257
326
  return;
258
327
 
259
- const isNumeric = builtins.isNumericTypeName(type.name.absolute);
260
- const isString = builtins.isStringTypeName(type.name.absolute);
328
+ const isNumeric = type.category === 'decimal' || type.category === 'integer';
329
+ const isString = type.category === 'string';
261
330
 
262
331
  if (!isString) {
263
332
  // 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);
333
+ const emptyValue = Object.keys( enumNode.enum )
334
+ .find( name => !enumNode.enum[name].value );
265
335
  if (emptyValue) {
266
336
  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
- });
337
+ warning( 'type-missing-value', [ failedEnum.location, failedEnum ], {
338
+ '#': isNumeric ? 'numeric' : 'std', name: emptyValue,
339
+ }, {
340
+ std: 'Missing value for non-string enum element $(NAME)',
341
+ numeric: 'Missing value for numeric enum element $(NAME)',
342
+ } );
273
343
  }
274
344
  }
275
345
 
@@ -286,29 +356,31 @@ function check( model ) { // = XSN
286
356
  (element.value.literal !== expectedType) &&
287
357
  (element.value.literal !== 'enum');
288
358
 
289
- for (const key of Object.keys(enumNode.enum)) {
359
+ for (const key of Object.keys( enumNode.enum )) {
290
360
  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
- });
361
+ if (hasWrongType( element )) {
362
+ const actualType = element.value.literal;
363
+ warning( 'type-unexpected-value', [ element.value.location, element ], {
364
+ '#': expectedType, name: key, prop: actualType || 'unknown',
365
+ }, {
366
+ std: 'Incorrect value type $(PROP) for enum element $(NAME)', // Not used
367
+ number: 'Expected numeric value for enum element $(NAME) but was $(PROP)',
368
+ string: 'Expected string value for enum element $(NAME) but was $(PROP)',
369
+ } );
370
+ }
301
371
  }
302
372
  }
303
373
 
304
- // TODO: check inside compiler as it is a compiler restriction - improve
305
374
  /**
375
+ * TODO: check inside compiler as it is a compiler restriction - improve
376
+ * TODO: Recursive check; use "parentProps" as other member checks
377
+ *
306
378
  * Non-recursive check if sub-elements have a "localized" keyword since this is
307
379
  * not yet supported.
308
380
  *
309
381
  * This check is not recursive to avoid a runtime overhead. Because of this it fails
310
382
  * to detect scenarios with indirections, e.g.
311
- *
383
+ * ```cds
312
384
  * type L : localized String;
313
385
  * type L1 : L;
314
386
  * type L2 : L1;
@@ -318,153 +390,104 @@ function check( model ) { // = XSN
318
390
  * subElement : L2;
319
391
  * }
320
392
  * }
393
+ * ```
321
394
  *
322
395
  * @param {XSN.Artifact} element
323
396
  */
324
397
  function checkLocalizedSubElement( element ) {
325
- if (element._parent.kind !== 'element')
398
+ if (element._parent?.kind !== 'element')
326
399
  return;
327
400
 
328
- const isLocalizedSubElement = element.localized && element.localized.val;
329
- if (isLocalizedSubElement || (element.type && isTypeLocalized(element.type._artifact))) {
401
+ const isLocalizedSubElement = element.localized?.val;
402
+ if (isLocalizedSubElement || element.type?._artifact?.localized?.val) {
330
403
  const loc = isLocalizedSubElement ? element.localized.location : element.type.location;
331
- warning('localized-sub-element', [ loc, element ],
332
- { type: element.type, '#': isLocalizedSubElement ? 'std' : 'type' },
333
- {
334
- std: 'Keyword "localized" is ignored for sub elements',
335
- type: 'Keyword "localized" in type $(TYPE) is ignored for sub elements',
336
- } );
337
- }
338
- return;
339
-
340
- // TODO: Recursive check
341
- function isTypeLocalized( type ) {
342
- return (type && type.localized && type.localized.val);
404
+ warning( 'localized-sub-element', [ loc, element ],
405
+ { type: element.type, '#': isLocalizedSubElement ? 'std' : 'type' },
406
+ {
407
+ std: 'Keyword "localized" is ignored for sub elements',
408
+ type: 'Keyword "localized" in type $(TYPE) is ignored for sub elements',
409
+ } );
343
410
  }
344
411
  }
345
412
 
346
413
  /**
347
- * Check that a primary key element is not an unmanaged association or
348
- * contains unmanaged associations
414
+ * Check that min and max cardinalities of 'art' have legal values
349
415
  *
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.
416
+ * TODO: move to define.js or parsers
353
417
  *
354
- * @param {any} element Element to check recursively
418
+ * @param {XSN.Artifact} art
355
419
  */
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)
420
+ function checkCardinality( art ) {
421
+ if (!art.cardinality)
377
422
  return;
378
423
 
379
424
  // Max cardinalities must be a positive number or '*'
380
425
  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
- });
393
- }
394
- }
395
- }
396
-
397
- // Min cardinality must be a non-negative number
398
- // Note: Already checked by parser (syntax error if -1 is used) and
399
- // from-csn.json (expected non-negative number)
400
- 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
- });
426
+ if (art.cardinality[prop]) {
427
+ const { literal, val, location } = art.cardinality[prop];
428
+ if (!(literal === 'number' && val > 0 || literal === 'string' && val === '*')) {
429
+ error( 'type-invalid-cardinality', [ location, art ],
430
+ { '#': prop, prop: val, otherprop: '*' } );
412
431
  }
413
432
  }
414
433
  }
415
434
 
416
435
  // If provided, min cardinality must not exceed max cardinality (note that
417
436
  // '*' 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
- }
429
- });
430
- }
431
-
432
- // TODO: make this part of the name resolution in the compiler
433
- // Check that queries in 'art' do not contain unmanaged associations in GROUP BY or ORDER BY
434
- function checkNoUnmanagedAssocsInGroupByOrderBy( query ) {
435
- const art = query._main; // TODO - remove, use query for semantic location
436
- for (const groupByEntry of query.groupBy || []) {
437
- if (groupByEntry._artifact && groupByEntry._artifact._effectiveType &&
438
- groupByEntry._artifact._effectiveType.on) {
439
- // Unmanaged association - complain
440
- error(null, [ groupByEntry.location, art ], {},
441
- 'Unmanaged associations are not allowed in GROUP BY');
442
- }
443
- }
444
- for (const orderByEntry of query.orderBy || []) {
445
- if (orderByEntry._artifact && orderByEntry._artifact._effectiveType &&
446
- orderByEntry._artifact._effectiveType.on) {
447
- // Unmanaged association - complain
448
- error(null, [ orderByEntry.location, art ], {},
449
- 'Unmanaged associations are not allowed in ORDER BY');
450
- }
451
- }
437
+ const pair = [
438
+ [ 'sourceMin', 'sourceMax', 'sourceVal' ],
439
+ [ 'targetMin', 'targetMax', 'targetVal' ],
440
+ ];
441
+ pair.forEach( ([ lhs, rhs, variant ]) => {
442
+ if (art.cardinality[lhs] && art.cardinality[rhs] &&
443
+ art.cardinality[rhs].literal === 'number' &&
444
+ art.cardinality[lhs].val > art.cardinality[rhs].val)
445
+ error( 'type-invalid-cardinality', [ art.cardinality.location, art ], { '#': variant } );
446
+ } );
452
447
  }
453
448
 
454
449
  function checkAssociation( elem ) {
450
+ if (!elem.target && !elem.targetAspect)
451
+ return;
455
452
  // TODO: yes, a check similar to this could make it into the compiler)
456
453
  // when virtual element is part of association
454
+ let fkCount = 0;
457
455
  if (elem.foreignKeys) {
458
456
  for (const k in elem.foreignKeys) {
457
+ ++fkCount;
459
458
  const key = elem.foreignKeys[k].targetElement;
460
- if (key && isVirtualElement(key._artifact))
461
- error('ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' });
459
+ if (key && isVirtualElement( key._artifact ))
460
+ error( 'ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' } );
462
461
  else if (key._artifact?.$syntax === 'calc' && !key._artifact.value.stored?.val)
463
462
  error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );
464
463
  }
465
464
  }
465
+ if (elem.default) {
466
+ if (!isBetaEnabled( model.options, 'associationDefault' )) {
467
+ error( 'type-unsupported-default', [ elem.default.location, elem ], {
468
+ '#': isComposition( model, elem ) ? 'comp' : 'std',
469
+ }, {
470
+ std: 'Unsupported default value on an association',
471
+ comp: 'Unsupported default value on a composition',
472
+ } );
473
+ }
474
+ else if (elem.targetAspect || elem.on || fkCount !== 1) {
475
+ const variant = (elem.targetAspect && 'targetAspect') || (elem.on && 'onCond') || 'multi';
476
+ error( 'type-unexpected-default', [ elem.default.location, elem ], {
477
+ '#': variant, keyword: 'default', count: fkCount,
478
+ } );
479
+ }
480
+ else {
481
+ const fkName = Object.keys( elem.foreignKeys )[0];
482
+ if (elem.foreignKeys[fkName].targetElement._artifact?._effectiveType?.elements) {
483
+ error( 'type-unexpected-default', [ elem.default.location, elem ], {
484
+ '#': 'structured', keyword: 'default', name: fkName,
485
+ } );
486
+ }
487
+ }
488
+ }
466
489
 
467
- checkOnCondition(elem);
490
+ checkOnCondition( elem );
468
491
  }
469
492
 
470
493
  function getBinaryOp( cond ) {
@@ -473,8 +496,9 @@ function check( model ) { // = XSN
473
496
  args[1] || op;
474
497
  }
475
498
 
476
- // A function like this could be part of the compiler
477
499
  /**
500
+ * TODO: A function like this could be part of the compiler
501
+ *
478
502
  * Check that the given type has no conflicts between its `type` property
479
503
  * and its `elements` or `items` property. For example if `type` is not
480
504
  * structured but the artifact has an `elements` property then the user
@@ -485,22 +509,22 @@ function check( model ) { // = XSN
485
509
  function checkTypeStructure( artifact ) {
486
510
  // Just a basic check. We do not check that the inner structure of `items`
487
511
  // is the same as the type but only that all are arrayed or structured.
488
- if (artifact.type && artifact.type._artifact) {
512
+ if (artifact.type?._artifact) {
489
513
  const finalType = artifact.type._artifact._effectiveType || artifact.type._artifact;
490
514
 
491
515
  if (artifact.items && !finalType.items) {
492
- warning('type-items-mismatch', [ artifact.type.location, artifact ],
493
- { type: artifact.type, prop: 'items' },
494
- 'Used type $(TYPE) is not arrayed and conflicts with $(PROP) property');
516
+ warning( 'type-items-mismatch', [ artifact.type.location, artifact ],
517
+ { type: artifact.type, prop: 'items' },
518
+ 'Used type $(TYPE) is not arrayed and conflicts with $(PROP) property' );
495
519
  }
496
520
  else if (artifact.elements && !finalType.elements) {
497
- warning('type-elements-mismatch', [ artifact.type.location, artifact ],
498
- { type: artifact.type, prop: 'elements' },
499
- 'Used type $(TYPE) is not structured and conflicts with $(PROP) property');
521
+ warning( 'type-elements-mismatch', [ artifact.type.location, artifact ],
522
+ { type: artifact.type, prop: 'elements' },
523
+ 'Used type $(TYPE) is not structured and conflicts with $(PROP) property' );
500
524
  }
501
525
  }
502
526
  if (artifact.items)
503
- checkTypeStructure(artifact.items);
527
+ checkTypeStructure( artifact.items );
504
528
  }
505
529
 
506
530
  /**
@@ -517,7 +541,7 @@ function check( model ) { // = XSN
517
541
  if (element.$inferred !== 'include') {
518
542
  for (const include of def.includes) {
519
543
  if (include._artifact?.elements?.[name] !== undefined)
520
- checkElementOverride( element, include._artifact.elements[name]);
544
+ checkElementOverride( element, include._artifact.elements[name] );
521
545
  }
522
546
  }
523
547
  }
@@ -532,12 +556,12 @@ function check( model ) { // = XSN
532
556
  const name = elem.name.id;
533
557
  // Position at type/struct, not name
534
558
  const loc = elem.type?.location || elem.elements?.[$location] || elem.location;
535
- error('ref-invalid-override', [ loc, elem ],
536
- { '#': prop, art: original._main, name });
559
+ error( 'ref-invalid-override', [ loc, elem ],
560
+ { '#': prop, art: original._main, name } );
537
561
  return false;
538
562
  }
539
563
  else if (original.elements &&
540
- !checkSubStructureOverride(elem, elem.elements, original.elements)) {
564
+ !checkSubStructureOverride( elem, elem.elements, original.elements )) {
541
565
  return false;
542
566
  }
543
567
  return true;
@@ -552,10 +576,10 @@ function check( model ) { // = XSN
552
576
  const orig = originals[element];
553
577
  if (elem === undefined) {
554
578
  const loc = [ elements[$location], user ];
555
- error('ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element });
556
- return false; // only report one
579
+ error( 'ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element } );
580
+ return false; // only report once
557
581
  }
558
- else if (!checkElementOverride(elem, orig)) {
582
+ else if (!checkElementOverride( elem, orig )) {
559
583
  return false;
560
584
  }
561
585
  }
@@ -564,8 +588,6 @@ function check( model ) { // = XSN
564
588
  }
565
589
 
566
590
 
567
- // Former checkExpressions.js ----------------------------------------------
568
-
569
591
  /**
570
592
  * Check a generic expression (or condition) for semantic validity.
571
593
  *
@@ -574,62 +596,80 @@ function check( model ) { // = XSN
574
596
  * @returns {void}
575
597
  */
576
598
  function checkGenericExpression( xpr, user ) {
577
- checkExpressionNotVirtual(xpr, user);
578
- checkExpressionAssociationUsage(xpr, user, false);
599
+ checkExpressionNotVirtual( xpr, user );
600
+ checkExpressionAssociationUsage( xpr, user, false );
601
+ if (xpr.op?.val === 'cast') {
602
+ requireExplicitTypeInSqlCast( xpr, user );
603
+ checkTypeArguments( xpr, user );
604
+ }
579
605
  }
580
606
 
581
607
  function checkExpressionNotVirtual( xpr, user ) {
582
- if (xpr._artifact && isVirtualElement(xpr._artifact))
583
- error('ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' });
608
+ if (xpr._artifact && isVirtualElement( xpr._artifact ))
609
+ error( 'ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' } );
584
610
  }
585
611
 
586
612
  function checkOnCondition( elem ) {
613
+ if (elem.$inferred === 'localized')
614
+ return; // ignore
615
+
587
616
  // TODO: Move to checkAssociation
588
617
  if (elem.on && !elem.on.$inferred) {
589
- visitExpression(elem.on, elem, (xpr, user) => {
590
- checkExpressionNotVirtual(xpr, user);
591
- checkExpressionAssociationUsage(xpr, user, true);
618
+ visitExpression( elem.on, elem, (xpr, user) => {
619
+ checkExpressionNotVirtual( xpr, user );
620
+ checkExpressionAssociationUsage( xpr, user, true );
621
+ // Essential check. Dependency handling for `on` conditions must change if
622
+ // this is allowed. See test3/Associations/Dependencies/.
592
623
  if (xpr._artifact?.$syntax === 'calc' && !xpr._artifact.value.stored?.val)
593
624
  error( 'ref-unexpected-calculated', [ xpr.location, user ], { '#': 'on' } );
594
- });
595
- if (isDollarSelfOrProjectionOperand(elem.on)) {
596
- // Bare $self usages are not allowed and don't work in A2J.
597
- error('expr-missing-comparison', [ elem.on.location, elem ], { id: '$self', op: '=' } );
598
- }
625
+ } );
599
626
  }
600
627
  }
601
628
 
602
629
  function checkSelectItemValue( elem ) {
603
- checkExpressionAssociationUsage(elem.value, elem, false);
604
-
605
- visitSubExpression(elem.value, elem, (xpr) => {
606
- checkExpressionNotVirtual(xpr, elem);
607
- checkExpressionAssociationUsage(xpr, elem, false);
608
- });
630
+ checkExpressionAssociationUsage( elem.value, elem, false );
631
+ // If a direct SQL-style cast() has no type, but type props, the compiler does not copy the type
632
+ // props/does not use the cast(). To avoid duplicate messages, only run this check if there is
633
+ // no explicit type, as otherwise we will check the cast() twice (once here, once via element).
634
+ if (elem.value?.op?.val === 'cast' && !elem.type) {
635
+ requireExplicitTypeInSqlCast( elem.value, elem );
636
+ checkTypeArguments( elem.value, elem );
637
+ }
638
+ visitSubExpression( elem.value, elem, (xpr) => {
639
+ checkGenericExpression( xpr, elem );
640
+ } );
609
641
  }
610
642
 
611
643
  function checkCalculatedElementValue( elem ) {
612
- visitExpression(elem.value, elem, (xpr, user) => {
613
- if (xpr._artifact) { // we only need to check artifact references
644
+ visitExpression( elem.value, elem, (xpr, user) => {
645
+ // We only need to check artifact references. To avoid false positives and conflicts
646
+ // with $self comparison-checks, ignore bare $self.
647
+ const isArtRef = xpr._artifact && !(xpr.path?.length === 1 &&
648
+ xpr.path[0]._navigation?.kind === '$self');
649
+ if (isArtRef) {
614
650
  const sourceLoc = xpr.path?.[xpr.path.length - 1].location || xpr.location;
615
- checkExpressionNotVirtual(xpr, user);
651
+ checkExpressionNotVirtual( xpr, user );
616
652
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
617
653
  // And users can't change structured to non-structured elements.
618
- if (!elem.$inferred && isStructuredElement(xpr._artifact))
619
- 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)
623
- error('ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' });
654
+ if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
655
+ error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
656
+ }
657
+ else if (xpr._artifact.target !== undefined) {
658
+ const variant = isComposition( model, xpr._artifact ) ? 'expr-comp' : 'expr';
659
+ error( 'ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant } );
660
+ }
661
+ else if (xpr._artifact.localized?.val && elem.value.stored?.val) {
662
+ error( 'ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' } );
663
+ }
624
664
  }
625
- });
665
+ } );
626
666
  // Calculated elements must not refer to keys, because that may lead to another
627
667
  // key in an SQL view, which is missing in OData (for on-read).
628
668
  // Following associations does not lead to this issue.
629
- if (elem.value.path && isKeyElement(elem.value._artifact) &&
630
- !followsAnAssociation(elem.value.path)) {
631
- error('ref-unexpected-key', [ elem.value.location, elem ], {},
632
- 'Calculated elements can\'t refer directly to key elements');
669
+ if (elem.value.path && isKeyElement( elem.value._artifact ) &&
670
+ !followsAnAssociation( elem.value.path )) {
671
+ error( 'ref-unexpected-key', [ elem.value.location, elem ], {},
672
+ 'Calculated elements can\'t refer directly to key elements' );
633
673
  }
634
674
  }
635
675
 
@@ -676,13 +716,6 @@ function check( model ) { // = XSN
676
716
  return false;
677
717
  }
678
718
 
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
719
  /**
687
720
  * Check a tree-like expression for semantic validity
688
721
  *
@@ -698,21 +731,15 @@ function check( model ) { // = XSN
698
731
  // Only check associations and $self if this is not a backlink-like
699
732
  // expression (a comparison of $self with an assoc).
700
733
  // We don't check token-stream-like 'xpr's.
701
- const isNotSelfComparison = xpr.op?.val !== 'xpr' &&
702
- !isBinaryDollarSelfComparisonWithAssoc(xpr);
703
- const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
704
- const op = getBinaryOp(xpr);
734
+ const args = Array.isArray( xpr.args ) ? xpr.args : Object.values( xpr.args );
735
+ const isNotSelfComparison = args.length > 0 && xpr.op?.val !== 'xpr' &&
736
+ !isBinaryDollarSelfComparisonWithAssoc( xpr );
705
737
 
706
738
  if (isNotSelfComparison) {
739
+ const op = getBinaryOp( xpr );
707
740
  for (const arg of args) {
708
- if (op?.val !== '=' && isDollarSelfOrProjectionOperand(arg)) {
709
- // `nary` operators don't have a "good" location; use $self in that case.
710
- const loc = (op?.location.endLine ? op : arg).location;
711
- error('expr-invalid-operator', [ loc, user ], { op: '=', id: '$self' });
712
- }
713
- else {
714
- checkExpressionIsNotAssocOrSelf(arg, user, allowAssocTail);
715
- }
741
+ if (arg && !(op?.val !== '=' && isDollarSelfOrProjectionOperand( arg )))
742
+ checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail );
716
743
  }
717
744
  }
718
745
  }
@@ -721,28 +748,23 @@ function check( model ) { // = XSN
721
748
  // Arg must not be an association and not $self
722
749
  // Only if path is not approved exists path (that is non-query position)
723
750
  if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
724
- if (arg.$expected === 'exists')
725
- error('ref-unexpected-assoc', [ arg.location, user ], { '#': 'expr' } );
726
- }
727
- else if (!allowAssocTail && isAssociationOperand(arg)) {
728
- error('ref-unexpected-assoc', [ arg.location, user ], { '#': 'expr' } );
751
+ if (arg.$expected === 'exists') {
752
+ const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
753
+ error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
754
+ }
729
755
  }
730
-
731
- if (isDollarSelfOrProjectionOperand(arg)) {
732
- error(null, [ arg.location, user ], { id: arg.path[0].id },
733
- '$(ID) can only be used as a value in a comparison to an association');
756
+ else if (!allowAssocTail && isAssociationOperand( arg )) {
757
+ const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
758
+ error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
734
759
  }
735
760
  }
736
761
 
737
- // Return true if 'arg' is an expression argument of type association or composition
762
+ /**
763
+ * Return true if 'arg' is an expression argument of type association or composition.
764
+ */
738
765
  function isAssociationOperand( arg ) {
739
- if (!arg.path) {
740
- // Not a path, hence not an association (literal, expression, function, whatever ...)
741
- return false;
742
- }
743
766
  // 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);
767
+ return !!arg._artifact?._effectiveType?.target;
746
768
  }
747
769
 
748
770
  /**
@@ -764,17 +786,20 @@ function check( model ) { // = XSN
764
786
  if (!xpr.op || !xpr.args)
765
787
  return false;
766
788
 
767
-
768
789
  // One argument must be "$self" and the other an assoc
769
790
  if (xpr.op.val === '=' && xpr.args.length === 2) {
770
791
  // Tree-ish expression from the compiler (not augmented)
771
- return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[1]) ||
772
- isAssociationOperand(xpr.args[1]) && isDollarSelfOrProjectionOperand(xpr.args[0]));
792
+ // eslint-disable-next-line max-len
793
+ return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[1] ) ||
794
+ // eslint-disable-next-line max-len
795
+ isAssociationOperand( xpr.args[1] ) && isDollarSelfOrProjectionOperand( xpr.args[0] ));
773
796
  }
774
797
  else if (xpr.args.length === 3 && xpr.args[1].val === '=') {
775
798
  // Tree-ish expression from the compiler (not augmented)
776
- return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[2]) ||
777
- isAssociationOperand(xpr.args[2]) && isDollarSelfOrProjectionOperand(xpr.args[0]));
799
+ // eslint-disable-next-line max-len
800
+ return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[2] ) ||
801
+ // eslint-disable-next-line max-len
802
+ isAssociationOperand( xpr.args[2] ) && isDollarSelfOrProjectionOperand( xpr.args[0] ));
778
803
  }
779
804
 
780
805
  // Nothing else qualifies
@@ -787,19 +812,19 @@ function check( model ) { // = XSN
787
812
  // Check the annotation assignments (if any) of 'annotatable', possibly using annotation
788
813
  // definitions from 'model'. Report errors on 'options.messages.
789
814
  //
790
- // TODO: rework completely
815
+ // TODO: rework completely!
791
816
 
792
817
  // Has been slightly adapted for model.vocabularies but comments need to be
793
818
  // adapted, etc.
794
819
  function checkAnnotationAssignment1( art, anno ) {
795
820
  // Sanity checks (ignore broken assignments)
796
- if (!anno.name || !anno.name.path || !anno.name.path.length)
821
+ if (!anno.name?.path?.length)
797
822
  return;
798
823
  // Annotation artifact for longest path step of annotation path
799
824
  let fromArtifact = null;
800
825
  let pathStepsFound = 0;
801
826
  for (let i = anno.name.path.length; i > 0; i--) {
802
- const absoluteName = anno.name.path.slice(0, i).map(path => path.id).join('.');
827
+ const absoluteName = anno.name.path.slice( 0, i ).map( path => path.id ).join( '.' );
803
828
  if (model.vocabularies[absoluteName]) {
804
829
  fromArtifact = model.vocabularies[absoluteName];
805
830
  pathStepsFound = i;
@@ -812,8 +837,8 @@ function check( model ) { // = XSN
812
837
  return;
813
838
  }
814
839
 
815
- const { artifact, endOfPath } = resolvePathFrom(anno.name.path.slice(pathStepsFound),
816
- fromArtifact);
840
+ const { artifact, endOfPath } = resolvePathFrom( anno.name.path.slice( pathStepsFound ),
841
+ fromArtifact );
817
842
 
818
843
  // Check what we actually want to check
819
844
  checkAnnotationAssignment( anno, artifact, endOfPath, art );
@@ -827,40 +852,39 @@ function check( model ) { // = XSN
827
852
  if (!annoDecl || annoDecl.artifacts && !elementDecl)
828
853
  return;
829
854
 
830
-
831
855
  // Must be an annotation if found
832
856
  if (annoDecl.kind !== 'annotation') // i.e namespace
833
857
  return;
834
858
 
835
859
  // Element must exist in annotation
836
860
  if (!elementDecl) {
837
- warning(null, [ anno.location || anno.name.location, art ],
838
- { name: pathName(anno.name.path), anno: annoDecl.name.absolute },
839
- 'Element $(NAME) not found for annotation $(ANNO)');
861
+ warning( null, [ anno.location || anno.name.location, art ],
862
+ { name: pathName( anno.name.path ), anno: annoDecl.name.absolute },
863
+ 'Element $(NAME) not found for annotation $(ANNO)' );
840
864
  return;
841
865
  }
842
866
 
843
867
  // Sanity checks
844
868
  if (!elementDecl._effectiveType)
845
- throw new CompilerAssertion(`Expecting annotation declaration to have _finalType: ${ JSON.stringify(annoDecl) }`);
869
+ throw new CompilerAssertion(`Expecting annotation declaration to have _finalType: ${ JSON.stringify( annoDecl ) }`);
846
870
 
847
871
 
848
872
  // Must have literal or path unless it is a boolean
849
- if (!anno.literal && !anno.path && getFinalTypeNameOf(elementDecl) !== 'cds.Boolean') {
850
- if (elementDecl.type && elementDecl.type._artifact.name.absolute) {
851
- warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
852
- { '#': 'type', type: elementDecl.type._artifact });
873
+ if (!anno.literal && !anno.path && getFinalTypeNameOf( elementDecl ) !== 'cds.Boolean') {
874
+ if (elementDecl.type?._artifact.name.absolute) {
875
+ warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
876
+ { '#': 'type', type: elementDecl.type._artifact } );
853
877
  }
854
878
  else {
855
- warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
856
- { '#': 'std', anno: anno.name.absolute });
879
+ warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
880
+ { '#': 'std', anno: anno.name.absolute } );
857
881
  }
858
882
 
859
883
  return;
860
884
  }
861
885
 
862
886
  // Value must be assignable to type
863
- checkValueAssignableTo(anno, anno, elementDecl, art);
887
+ checkValueAssignableTo( anno, anno, elementDecl, art );
864
888
  }
865
889
 
866
890
  // Check that annotation assignment 'value' (having 'path or 'literal' and
@@ -868,7 +892,7 @@ function check( model ) { // = XSN
868
892
  // if not
869
893
  function checkValueAssignableTo( annoDef, value, elementDecl, art ) {
870
894
  // FIXME: We currently do not have any element declaration that could match
871
- // a 'path' value, so we simply leave those alone
895
+ // a 'path' value, so we simply leave those alone
872
896
  if (value.path)
873
897
  return;
874
898
 
@@ -879,12 +903,12 @@ function check( model ) { // = XSN
879
903
  if (elementDecl._effectiveType.items) {
880
904
  // Make sure we have an array value
881
905
  if (value.literal !== 'array') {
882
- warning(null, loc, { anno }, 'An array value is required for annotation $(ANNO)');
906
+ warning( null, loc, { anno }, 'An array value is required for annotation $(ANNO)' );
883
907
  return;
884
908
  }
885
909
  // Check each element
886
910
  for (const valueItem of value.val)
887
- checkValueAssignableTo(value, valueItem, elementDecl._effectiveType.items, art);
911
+ checkValueAssignableTo( value, valueItem, elementDecl._effectiveType.items, art );
888
912
 
889
913
  return;
890
914
  }
@@ -892,7 +916,7 @@ function check( model ) { // = XSN
892
916
  // Struct expected (can only happen within arrays)?
893
917
  if (elementDecl._effectiveType.elements) {
894
918
  if (value.literal !== 'struct') {
895
- warning(null, loc, { anno }, 'A struct value is required here for annotation $(ANNO)');
919
+ warning( null, loc, { anno }, 'A struct value is required here for annotation $(ANNO)' );
896
920
  return;
897
921
  }
898
922
  // FIXME: Should check each element
@@ -900,44 +924,45 @@ function check( model ) { // = XSN
900
924
  }
901
925
 
902
926
  // Handle each (primitive) expected element type separately
903
- const type = getFinalTypeNameOf(elementDecl);
904
- if (builtins.isStringTypeName(type)) {
927
+ // TODO: Don't rely on name; use actual type
928
+ const type = getFinalTypeNameOf( elementDecl );
929
+ if (builtins.isStringTypeName( type )) {
905
930
  if (value.literal !== 'string' && value.literal !== 'enum' &&
906
931
  !elementDecl._effectiveType.enum) {
907
- warning(null, loc, { type, anno },
908
- 'A string value is required for type $(TYPE) for annotation $(ANNO)');
932
+ warning( null, loc, { type, anno },
933
+ 'A string value is required for type $(TYPE) for annotation $(ANNO)' );
909
934
  }
910
935
  }
911
- else if (builtins.isBinaryTypeName(type)) {
936
+ else if (builtins.isBinaryTypeName( type )) {
912
937
  if (value.literal !== 'string' && value.literal !== 'x') {
913
- warning(null, loc, { type, anno },
914
- 'A hexadecimal string value is required for type $(TYPE) for annotation $(ANNO)');
938
+ warning( null, loc, { type, anno },
939
+ 'A hexadecimal string value is required for type $(TYPE) for annotation $(ANNO)' );
915
940
  }
916
941
  }
917
- else if (builtins.isNumericTypeName(type)) {
942
+ else if (builtins.isNumericTypeName( type )) {
918
943
  if (value.literal !== 'number' && value.literal !== 'enum' &&
919
944
  !elementDecl._effectiveType.enum) {
920
- warning(null, loc, { type, anno },
921
- 'A numerical value is required for type $(TYPE) for annotation $(ANNO)');
945
+ warning( null, loc, { type, anno },
946
+ 'A numerical value is required for type $(TYPE) for annotation $(ANNO)' );
922
947
  }
923
948
  }
924
- else if (builtins.isDateOrTimeTypeName(type)) {
949
+ else if (builtins.isDateOrTimeTypeName( type )) {
925
950
  if (value.literal !== 'date' && value.literal !== 'time' &&
926
951
  value.literal !== 'timestamp' && value.literal !== 'string') {
927
- warning(null, loc, { type, anno },
928
- // eslint-disable-next-line max-len
929
- 'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)');
952
+ warning( null, loc, { type, anno },
953
+ // eslint-disable-next-line max-len
954
+ 'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)' );
930
955
  }
931
956
  }
932
- else if (builtins.isBooleanTypeName(type)) {
957
+ else if (builtins.isBooleanTypeName( type )) {
933
958
  if (value.literal && value.literal !== 'boolean') {
934
- warning(null, loc, { type, anno },
935
- 'A boolean value is required for type $(TYPE) for annotation $(ANNO)');
959
+ warning( null, loc, { type, anno },
960
+ 'A boolean value is required for type $(TYPE) for annotation $(ANNO)' );
936
961
  }
937
962
  }
938
- else if (builtins.isRelationTypeName(type) || builtins.isGeoTypeName(type)) {
939
- warning(null, loc, { type, anno },
940
- 'Type $(TYPE) can\'t be assigned a value for annotation $(ANNO)');
963
+ else if (builtins.isRelationTypeName( type ) || builtins.isGeoTypeName( type )) {
964
+ warning( null, loc, { type, anno },
965
+ 'Type $(TYPE) can\'t be assigned a value for annotation $(ANNO)' );
941
966
  }
942
967
  else if (!elementDecl._effectiveType.enum) {
943
968
  throw new CompilerAssertion(`Unknown primitive type name: ${ type }`);
@@ -950,22 +975,22 @@ function check( model ) { // = XSN
950
975
  // Enum symbol provided and expected
951
976
  if (!expectedEnum[value.sym.id]) {
952
977
  // ... but no such constant
953
- warning(null, loc, { id: `#${ value.sym.id }`, anno }, 'Enum symbol $(ID) not found in enum for annotation $(ANNO)');
978
+ warning( null, loc, { id: `#${ value.sym.id }`, anno }, 'Enum symbol $(ID) not found in enum for annotation $(ANNO)' );
954
979
  }
955
980
  }
956
981
  else {
957
982
  // Enum symbol provided but not expected
958
- warning(null, loc, { id: `#${ value.sym.id }`, type, anno },
959
- 'Can\'t use enum symbol $(ID) for non-enum type $(TYPE) for annotation $(ANNO)');
983
+ warning( null, loc, { id: `#${ value.sym.id }`, type, anno },
984
+ 'Can\'t use enum symbol $(ID) for non-enum type $(TYPE) for annotation $(ANNO)' );
960
985
  }
961
986
  }
962
987
  else if (expectedEnum) {
963
988
  // Enum symbol not provided but expected
964
- const hasValidValue = Object.keys(expectedEnum)
965
- .some(symbol => getEnumValue(expectedEnum[symbol]) === value.val);
989
+ const hasValidValue = Object.keys( expectedEnum )
990
+ .some( symbol => getEnumValue( expectedEnum[symbol] ) === value.val );
966
991
  if (!hasValidValue) {
967
992
  // ... and none of the valid enum symbols matches the value
968
- warning(null, loc, { anno }, 'An enum value is required for annotation $(ANNO)');
993
+ warning( null, loc, { anno }, 'An enum value is required for annotation $(ANNO)' );
969
994
  }
970
995
  }
971
996
  }
@@ -978,8 +1003,7 @@ function check( model ) { // = XSN
978
1003
  return null;
979
1004
  }
980
1005
 
981
- // TODO: remove the following
982
-
1006
+ // TODO: remove
983
1007
  // Return the artifact (and possibly, its element) found by following 'path'
984
1008
  // starting at 'from'. The return value is an object { artifact, endOfPath }
985
1009
  // with 'artifact' being the last artifact encountered on 'path' (or
@@ -1001,9 +1025,10 @@ function check( model ) { // = XSN
1001
1025
  // Continue search with next path step
1002
1026
  const nextStepEnv = (from._effectiveType || from).artifacts ||
1003
1027
  from._effectiveType?.elements || [];
1004
- return resolvePathFrom(path.slice(1), nextStepEnv[path[0].id], result);
1028
+ return resolvePathFrom( path.slice(1), nextStepEnv[path[0].id], result );
1005
1029
  }
1006
1030
 
1031
+ // TODO: remove
1007
1032
  // Return the absolute name of the final type of 'node'. May return 'undefined'
1008
1033
  // for anonymous types. DO NOT USE THIS function, it has several assumptions
1009
1034
  // which are not necessarily true.
@@ -1016,7 +1041,7 @@ function check( model ) { // = XSN
1016
1041
  }
1017
1042
 
1018
1043
  /**
1019
- * Ensure that the `locale` element of sap.common.TextsAspects
1044
+ * Ensure that the `locale` element of `sap.common.TextsAspects`
1020
1045
  * is a string type. This is required by CAP runtimes to work properly.
1021
1046
  *
1022
1047
  * @param {XSN.Model} model
@@ -1026,17 +1051,16 @@ function checkSapCommonTextsAspects( model ) {
1026
1051
  const locale = model.definitions[name]?.elements?.locale;
1027
1052
  if (locale) {
1028
1053
  // `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') {
1054
+ if (locale._effectiveType !== model.definitions['cds.String']) {
1031
1055
  const hasCommonLocale = !!model.definitions['sap.common.Locale'];
1032
1056
  const { error } = model.$messageFunctions;
1033
- error('def-invalid-element-type', [ locale.type.location, locale ], {
1057
+ error( 'def-invalid-element-type', [ locale.type.location, locale ], {
1034
1058
  '#': hasCommonLocale ? 'texts-aspect-locale' : 'std',
1035
1059
  art: name,
1036
1060
  elemref: 'locale',
1037
1061
  type: 'cds.String',
1038
1062
  othertype: 'sap.common.Locale',
1039
- });
1063
+ } );
1040
1064
  }
1041
1065
  }
1042
1066
  }
@@ -1050,12 +1074,11 @@ function checkSapCommonTextsAspects( model ) {
1050
1074
  function checkSapCommonLocale( model ) {
1051
1075
  const localeArt = model.definitions['sap.common.Locale'];
1052
1076
  if (localeArt) {
1053
- const type = localeArt._effectiveType;
1054
- if (type?.name?.absolute !== 'cds.String') {
1077
+ if (localeArt._effectiveType !== model.definitions['cds.String']) {
1055
1078
  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');
1079
+ message( 'type-expected-builtin', [ localeArt.name.location, localeArt ],
1080
+ { name: 'sap.common.Locale' },
1081
+ 'Expected $(NAME) to be a string type' );
1059
1082
  }
1060
1083
  }
1061
1084
  }
@@ -1065,13 +1088,16 @@ function checkSapCommonLocale( model ) {
1065
1088
  * Visits each expression.
1066
1089
  *
1067
1090
  * TODO: Properly visit expressions; will be improved step by step;
1068
- * Currently only replaces old foreachPath().
1091
+ * Currently only replaces old foreachPath(), which had very poor performance.
1069
1092
  *
1070
1093
  * @param {any} xpr
1071
1094
  * @param {XSN.Artifact} user
1072
1095
  * @param {(xpr: any, user: any, parentExpr: any) => void} callback
1073
1096
  */
1074
1097
  function visitExpression( xpr, user, callback ) {
1098
+ if (!xpr)
1099
+ return; // e.g. parse error
1100
+
1075
1101
  callback( xpr, user, null );
1076
1102
  visitSubExpression( xpr, user, callback );
1077
1103
  }
@@ -1085,12 +1111,14 @@ function visitExpression( xpr, user, callback ) {
1085
1111
  */
1086
1112
  function visitSubExpression( xpr, user, callback ) {
1087
1113
  if (xpr.args) {
1088
- const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
1114
+ const args = Array.isArray( xpr.args ) ? xpr.args : Object.values( xpr.args );
1089
1115
  // Check for illegal argument usage within the expression
1090
1116
  for (const arg of args) {
1091
- callback( arg, user, xpr.args );
1092
- // Recursively traverse the argument expression
1093
- visitSubExpression(arg, user, callback);
1117
+ if (arg) { // null for parse errors
1118
+ callback( arg, user, xpr.args );
1119
+ // Recursively traverse the argument expression
1120
+ visitSubExpression( arg, user, callback );
1121
+ }
1094
1122
  }
1095
1123
  }
1096
1124
 
@@ -1098,11 +1126,31 @@ function visitSubExpression( xpr, user, callback ) {
1098
1126
  for (const arg of xpr.path) {
1099
1127
  if (arg.where) {
1100
1128
  callback( arg.where, user, arg );
1101
- visitSubExpression(arg.where, user, callback);
1129
+ visitSubExpression( arg.where, user, callback );
1102
1130
  }
1103
1131
  }
1104
1132
  }
1105
1133
  }
1106
1134
 
1135
+ /**
1136
+ * Whether the given element is a composition.
1137
+ * TODO: `type T: Composition of E; entity V { e: T default 3 };`
1138
+ * See also getUnderlyingBuiltinType()/compositionTextVariant() in utils.js.
1139
+ *
1140
+ * @return {boolean}
1141
+ */
1142
+ function isComposition( model, elem ) {
1143
+ elem = elem?._effectiveType;
1144
+ if (!elem || !elem.target)
1145
+ return false;
1146
+ do {
1147
+ if (elem.type?._artifact === model.definitions['cds.Composition'])
1148
+ return true;
1149
+ // Because inferred elements don't have a direct `type` property,
1150
+ // we need to go along the origin chain.
1151
+ elem = elem._origin;
1152
+ } while (elem);
1153
+ return false;
1154
+ }
1107
1155
 
1108
1156
  module.exports = check;