@sap/cds-compiler 4.1.2 → 4.2.4

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 (74) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/cdsc.js +6 -3
  3. package/doc/CHANGELOG_BETA.md +5 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +2 -2
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +24 -24
  8. package/lib/base/message-registry.js +41 -6
  9. package/lib/base/messages.js +7 -0
  10. package/lib/base/model.js +38 -8
  11. package/lib/checks/elements.js +11 -10
  12. package/lib/checks/manyNavigations.js +33 -0
  13. package/lib/checks/onConditions.js +5 -2
  14. package/lib/checks/queryNoDbArtifacts.js +2 -3
  15. package/lib/checks/selectItems.js +4 -55
  16. package/lib/checks/utils.js +3 -2
  17. package/lib/checks/validator.js +3 -1
  18. package/lib/compiler/.eslintrc.json +2 -1
  19. package/lib/compiler/assert-consistency.js +27 -24
  20. package/lib/compiler/base.js +6 -2
  21. package/lib/compiler/builtins.js +34 -34
  22. package/lib/compiler/checks.js +179 -208
  23. package/lib/compiler/classes.js +2 -2
  24. package/lib/compiler/cycle-detector.js +6 -6
  25. package/lib/compiler/define.js +66 -45
  26. package/lib/compiler/extend.js +81 -72
  27. package/lib/compiler/finalize-parse-cdl.js +26 -26
  28. package/lib/compiler/generate.js +61 -45
  29. package/lib/compiler/index.js +47 -49
  30. package/lib/compiler/kick-start.js +8 -7
  31. package/lib/compiler/moduleLayers.js +1 -1
  32. package/lib/compiler/populate.js +42 -35
  33. package/lib/compiler/propagator.js +6 -6
  34. package/lib/compiler/resolve.js +170 -126
  35. package/lib/compiler/shared.js +122 -45
  36. package/lib/compiler/tweak-assocs.js +93 -40
  37. package/lib/compiler/utils.js +15 -12
  38. package/lib/edm/.eslintrc.json +40 -1
  39. package/lib/edm/annotations/genericTranslation.js +721 -707
  40. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  41. package/lib/edm/csn2edm.js +389 -378
  42. package/lib/edm/edm.js +678 -772
  43. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  44. package/lib/edm/edmInboundChecks.js +29 -27
  45. package/lib/edm/edmPreprocessor.js +686 -646
  46. package/lib/edm/edmUtils.js +277 -296
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +1 -1
  49. package/lib/gen/languageParser.js +1253 -1276
  50. package/lib/json/from-csn.js +34 -4
  51. package/lib/json/to-csn.js +4 -4
  52. package/lib/language/language.g4 +2 -5
  53. package/lib/main.d.ts +61 -1
  54. package/lib/model/csnUtils.js +31 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +37 -2
  57. package/lib/modelCompare/utils/filter.js +1 -1
  58. package/lib/optionProcessor.js +15 -3
  59. package/lib/render/toCdl.js +30 -4
  60. package/lib/render/toSql.js +5 -9
  61. package/lib/render/utils/common.js +8 -6
  62. package/lib/transform/db/applyTransformations.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/constraints.js +47 -17
  65. package/lib/transform/db/expansion.js +133 -50
  66. package/lib/transform/db/flattening.js +75 -7
  67. package/lib/transform/forOdata.js +4 -1
  68. package/lib/transform/forRelationalDB.js +80 -62
  69. package/lib/transform/localized.js +91 -54
  70. package/lib/transform/transformUtils.js +9 -10
  71. package/lib/utils/file.js +7 -7
  72. package/lib/utils/moduleResolve.js +210 -121
  73. package/lib/utils/objectUtils.js +1 -1
  74. package/package.json +5 -5
@@ -15,13 +15,14 @@ const {
15
15
  forEachGeneric,
16
16
  forEachDefinition,
17
17
  forEachMember,
18
+ forEachMemberRecursively,
18
19
  isBetaEnabled,
19
20
  } = require('../base/model');
20
21
  const { CompilerAssertion } = require('../base/error');
21
22
  const { pathName } = require('./utils');
22
- const { forEachMemberRecursively } = require('../model/csnUtils');
23
23
  const { typeParameters } = require('./builtins');
24
- const $location = Symbol.for('cds.$location');
24
+
25
+ const $location = Symbol.for( 'cds.$location' );
25
26
 
26
27
  /**
27
28
  * Run compiler checks on the given XSN model.
@@ -38,13 +39,14 @@ function check( model ) {
38
39
 
39
40
  forEachDefinition( model, checkDefinition );
40
41
  forEachGeneric( model, 'vocabularies', checkAnnotationDefinition );
42
+
41
43
  return;
42
44
 
43
45
  function checkDefinition( def ) {
44
46
  checkGenericConstruct( def );
45
47
  if (def.includes && def.elements)
46
48
  checkElementIncludeOverride( def );
47
- forEachMember( def, member => checkMember(member) );
49
+ forEachMember( def, member => checkMember( member ) );
48
50
  if (def.$queries)
49
51
  def.$queries.forEach( checkQuery );
50
52
  }
@@ -55,7 +57,7 @@ function check( model ) {
55
57
  forEachMemberRecursively( art, (member) => {
56
58
  if (member.localized?.val)
57
59
  warning( 'def-unexpected-localized-anno', [ member.localized.location, member ] );
58
- });
60
+ } );
59
61
  }
60
62
 
61
63
  function checkGenericConstruct( art ) {
@@ -92,8 +94,8 @@ function check( model ) {
92
94
  const isKey = parentProps.key?.val || elem.key?.val;
93
95
  const isVirtual = parentProps.virtual?.val || elem.virtual?.val;
94
96
  if (isKey && isVirtual) {
95
- error('def-unexpected-key', [ isKey.location, elem ],
96
- { '#': 'virtual', art: elem.name.element, prop: 'key' });
97
+ error( 'def-unexpected-key', [ isKey.location, elem ],
98
+ { '#': 'virtual', art: elem.name.element, prop: 'key' } );
97
99
  }
98
100
  }
99
101
 
@@ -119,9 +121,11 @@ function check( model ) {
119
121
  // TODO: Move a corrected version of this check to definer (but do not rely on it!):
120
122
  // The code below misses to consider CSN input!
121
123
  // Maybe remove the check? But consider runtimes that rely on '.' as element separator.
122
- if (construct.name.id?.includes( '.' )) {
123
- error(null, [ construct.name.location, construct ], {},
124
- 'The character \'.\' is not allowed in identifiers');
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
+ }
125
129
  }
126
130
  }
127
131
 
@@ -140,7 +144,7 @@ function check( model ) {
140
144
  if (art.items)
141
145
  checkTypeArguments( art.items, art );
142
146
 
143
- const actualParams = typeParameters.list.filter(param => art[param] !== undefined);
147
+ const actualParams = typeParameters.list.filter( param => art[param] !== undefined );
144
148
  if (actualParams.length === 0)
145
149
  return;
146
150
 
@@ -156,17 +160,17 @@ function check( model ) {
156
160
  return; // e.g. illegal definition references, cycles, ...
157
161
  }
158
162
  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)');
163
+ error( 'type-missing-type', [ art.location, user ],
164
+ { otherprop: 'type', prop: actualParams[0] },
165
+ 'Missing $(OTHERPROP) property next to $(PROP)' );
162
166
  return;
163
167
  }
164
168
 
165
169
  const expectedParams = effectiveType.parameters &&
166
- effectiveType.parameters.map(p => p.name || p) || [];
170
+ effectiveType.parameters.map( p => p.name || p ) || [];
167
171
 
168
172
  for (const param of actualParams) {
169
- if (!expectedParams.includes(param)) {
173
+ if (!expectedParams.includes( param )) {
170
174
  // Whether the type ref itself is a builtin or a custom type with a builtin as base.
171
175
  let variant;
172
176
  if ((art.type?._artifact || art._effectiveType).builtin)
@@ -176,18 +180,18 @@ function check( model ) {
176
180
  else // effectiveType is not a builtin -> array or structured
177
181
  variant = 'non-scalar';
178
182
 
179
- error('type-unexpected-argument', [ art[param].location, user ], {
183
+ error( 'type-unexpected-argument', [ art[param].location, user ], {
180
184
  '#': variant, prop: param, art: art.type || art._effectiveType, type: effectiveType,
181
- });
185
+ } );
182
186
  break; // Avoid spam: Only emit the first error.
183
187
  }
184
- else if (!typeParameters.expectedLiteralsFor[param].includes(art[param].literal)) {
185
- error('type-unexpected-argument', [ art[param].location, user ], {
188
+ else if (!typeParameters.expectedLiteralsFor[param].includes( art[param].literal )) {
189
+ error( 'type-unexpected-argument', [ art[param].location, user ], {
186
190
  '#': 'incorrect-type',
187
191
  prop: param,
188
192
  code: art[param].literal,
189
193
  names: typeParameters.expectedLiteralsFor[param],
190
- });
194
+ } );
191
195
  break; // Avoid spam: Only emit the first error.
192
196
  }
193
197
  }
@@ -195,8 +199,8 @@ function check( model ) {
195
199
 
196
200
  function requireExplicitTypeInSqlCast( art, user ) {
197
201
  if (!art.type) {
198
- error('expr-missing-type', [ art.location, user ], { },
199
- 'Missing type in SQL cast function');
202
+ error( 'expr-missing-type', [ art.location, user ], { },
203
+ 'Missing type in SQL cast function' );
200
204
  }
201
205
  }
202
206
 
@@ -205,9 +209,9 @@ function check( model ) {
205
209
  const type = elem._effectiveType;
206
210
  // See discussion issue #6520: should we allow all scalar types?
207
211
  if (!type || !type.builtin || type.category !== 'string') {
208
- info('ref-expecting-localized-string', [ elem.type?.location, elem ],
209
- { keyword: 'localized' },
210
- '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)' );
211
215
  }
212
216
  }
213
217
 
@@ -217,36 +221,35 @@ function check( model ) {
217
221
  // original element is localized but not key, as that would have
218
222
  // already resulted in a warning by localized.js
219
223
  if (elem._origin?.localized?.val && !elem._origin.key?.val) {
220
- warning('def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' },
221
- 'Keyword $(KEYWORD) is ignored for primary keys');
224
+ warning( 'def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' },
225
+ 'Keyword $(KEYWORD) is ignored for primary keys' );
222
226
  }
223
227
  }
224
228
  }
225
229
 
226
230
  function checkQuery( query ) {
227
- checkNoUnmanagedAssocsInGroupByOrderBy( query );
228
231
  // TODO: check too simple (just one source), as most of those in this file
229
232
  // Check expressions in the various places where they may occur
230
233
  if (query.from)
231
- visitSubExpression(query.from, query, checkGenericExpression);
234
+ visitSubExpression( query.from, query, checkGenericExpression );
232
235
 
233
236
  if (query.where)
234
- visitExpression(query.where, query, checkGenericExpression);
237
+ visitExpression( query.where, query, checkGenericExpression );
235
238
 
236
239
  if (query.groupBy) {
237
240
  for (const groupByEntry of query.groupBy)
238
- visitExpression(groupByEntry, query, checkGenericExpression);
241
+ visitExpression( groupByEntry, query, checkGenericExpression );
239
242
  }
240
243
  if (query.having)
241
- visitExpression(query.having, query, checkGenericExpression);
244
+ visitExpression( query.having, query, checkGenericExpression );
242
245
 
243
246
  if (query.orderBy) {
244
247
  for (const orderByEntry of query.orderBy)
245
- visitExpression(orderByEntry, query, checkGenericExpression);
248
+ visitExpression( orderByEntry, query, checkGenericExpression );
246
249
  }
247
250
  if (query.mixin) {
248
251
  for (const mixinName in query.mixin)
249
- checkAssociation(query.mixin[mixinName]);
252
+ checkAssociation( query.mixin[mixinName] );
250
253
  }
251
254
  }
252
255
 
@@ -265,8 +268,8 @@ function check( model ) {
265
268
  // Special handling to print a more detailed error message.
266
269
  // Other cases like `null` as enum value are handled in `checkEnumValueType()`
267
270
  if (type === 'enum') {
268
- warning('ref-unexpected-enum', [ loc, enumNode ], {},
269
- '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' );
270
273
  }
271
274
  }
272
275
 
@@ -297,17 +300,17 @@ function check( model ) {
297
300
  else if (type.items)
298
301
  typeClass = 'items';
299
302
 
300
- error('type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, {
303
+ error( 'type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, {
301
304
  std: 'Only builtin types are allowed as enums',
302
305
  binary: 'Binary types are not allowed as enums',
303
306
  relation: 'Relational types are not allowed as enums',
304
307
  struct: 'Structured types are not allowed as enums',
305
308
  items: 'Arrayed types are not allowed as enums',
306
- });
309
+ } );
307
310
  return;
308
311
  }
309
312
 
310
- checkEnumValue(enumNode);
313
+ checkEnumValue( enumNode );
311
314
  }
312
315
 
313
316
  /**
@@ -327,16 +330,16 @@ function check( model ) {
327
330
 
328
331
  if (!isString) {
329
332
  // Non-string enums MUST have a value as the value is only deducted for string types.
330
- const emptyValue = Object.keys(enumNode.enum)
331
- .find(name => !enumNode.enum[name].value);
333
+ const emptyValue = Object.keys( enumNode.enum )
334
+ .find( name => !enumNode.enum[name].value );
332
335
  if (emptyValue) {
333
336
  const failedEnum = enumNode.enum[emptyValue];
334
- warning('type-missing-value', [ failedEnum.location, failedEnum ], {
337
+ warning( 'type-missing-value', [ failedEnum.location, failedEnum ], {
335
338
  '#': isNumeric ? 'numeric' : 'std', name: emptyValue,
336
339
  }, {
337
340
  std: 'Missing value for non-string enum element $(NAME)',
338
341
  numeric: 'Missing value for numeric enum element $(NAME)',
339
- });
342
+ } );
340
343
  }
341
344
  }
342
345
 
@@ -353,17 +356,17 @@ function check( model ) {
353
356
  (element.value.literal !== expectedType) &&
354
357
  (element.value.literal !== 'enum');
355
358
 
356
- for (const key of Object.keys(enumNode.enum)) {
359
+ for (const key of Object.keys( enumNode.enum )) {
357
360
  const element = enumNode.enum[key];
358
- if (hasWrongType(element)) {
361
+ if (hasWrongType( element )) {
359
362
  const actualType = element.value.literal;
360
- warning('type-unexpected-value', [ element.value.location, element ], {
361
- '#': expectedType, name: key, prop: actualType,
363
+ warning( 'type-unexpected-value', [ element.value.location, element ], {
364
+ '#': expectedType, name: key, prop: actualType || 'unknown',
362
365
  }, {
363
366
  std: 'Incorrect value type $(PROP) for enum element $(NAME)', // Not used
364
367
  number: 'Expected numeric value for enum element $(NAME) but was $(PROP)',
365
368
  string: 'Expected string value for enum element $(NAME) but was $(PROP)',
366
- });
369
+ } );
367
370
  }
368
371
  }
369
372
  }
@@ -392,18 +395,18 @@ function check( model ) {
392
395
  * @param {XSN.Artifact} element
393
396
  */
394
397
  function checkLocalizedSubElement( element ) {
395
- if (element._parent.kind !== 'element')
398
+ if (element._parent?.kind !== 'element')
396
399
  return;
397
400
 
398
401
  const isLocalizedSubElement = element.localized?.val;
399
402
  if (isLocalizedSubElement || element.type?._artifact?.localized?.val) {
400
403
  const loc = isLocalizedSubElement ? element.localized.location : element.type.location;
401
- warning('localized-sub-element', [ loc, element ],
402
- { type: element.type, '#': isLocalizedSubElement ? 'std' : 'type' },
403
- {
404
- std: 'Keyword "localized" is ignored for sub elements',
405
- type: 'Keyword "localized" in type $(TYPE) is ignored for sub elements',
406
- } );
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
+ } );
407
410
  }
408
411
  }
409
412
 
@@ -429,51 +432,18 @@ function check( model ) {
429
432
  }
430
433
  }
431
434
 
432
- // Min cardinality must be a non-negative number
433
- // Note: Already checked by parser (syntax error if -1 is used) and
434
- // from-csn.json (expected non-negative number)
435
- for (const prop of [ 'sourceMin', 'targetMin' ]) {
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 } );
440
- }
441
- }
442
-
443
435
  // If provided, min cardinality must not exceed max cardinality (note that
444
436
  // '*' is considered to be >= any number)
445
437
  const pair = [
446
438
  [ 'sourceMin', 'sourceMax', 'sourceVal' ],
447
439
  [ 'targetMin', 'targetMax', 'targetVal' ],
448
440
  ];
449
- pair.forEach(([ lhs, rhs, variant ]) => {
441
+ pair.forEach( ([ lhs, rhs, variant ]) => {
450
442
  if (art.cardinality[lhs] && art.cardinality[rhs] &&
451
443
  art.cardinality[rhs].literal === 'number' &&
452
444
  art.cardinality[lhs].val > art.cardinality[rhs].val)
453
445
  error( 'type-invalid-cardinality', [ art.cardinality.location, art ], { '#': variant } );
454
- });
455
- }
456
-
457
- // TODO: make this part of the name resolution in the compiler
458
- // Check that queries in 'art' do not contain unmanaged associations in GROUP BY or ORDER BY
459
- function checkNoUnmanagedAssocsInGroupByOrderBy( query ) {
460
- const art = query._main; // TODO - remove, use query for semantic location
461
- for (const groupByEntry of query.groupBy || []) {
462
- if (groupByEntry._artifact && groupByEntry._artifact._effectiveType &&
463
- groupByEntry._artifact._effectiveType.on) {
464
- // Unmanaged association - complain
465
- error(null, [ groupByEntry.location, art ], {},
466
- 'Unmanaged associations are not allowed in GROUP BY');
467
- }
468
- }
469
- for (const orderByEntry of query.orderBy || []) {
470
- if (orderByEntry._artifact && orderByEntry._artifact._effectiveType &&
471
- orderByEntry._artifact._effectiveType.on) {
472
- // Unmanaged association - complain
473
- error(null, [ orderByEntry.location, art ], {},
474
- 'Unmanaged associations are not allowed in ORDER BY');
475
- }
476
- }
446
+ } );
477
447
  }
478
448
 
479
449
  function checkAssociation( elem ) {
@@ -486,8 +456,8 @@ function check( model ) {
486
456
  for (const k in elem.foreignKeys) {
487
457
  ++fkCount;
488
458
  const key = elem.foreignKeys[k].targetElement;
489
- if (key && isVirtualElement(key._artifact))
490
- error('ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' });
459
+ if (key && isVirtualElement( key._artifact ))
460
+ error( 'ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' } );
491
461
  else if (key._artifact?.$syntax === 'calc' && !key._artifact.value.stored?.val)
492
462
  error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );
493
463
  }
@@ -508,7 +478,7 @@ function check( model ) {
508
478
  } );
509
479
  }
510
480
  else {
511
- const fkName = Object.keys(elem.foreignKeys)[0];
481
+ const fkName = Object.keys( elem.foreignKeys )[0];
512
482
  if (elem.foreignKeys[fkName].targetElement._artifact?._effectiveType?.elements) {
513
483
  error( 'type-unexpected-default', [ elem.default.location, elem ], {
514
484
  '#': 'structured', keyword: 'default', name: fkName,
@@ -517,7 +487,7 @@ function check( model ) {
517
487
  }
518
488
  }
519
489
 
520
- checkOnCondition(elem);
490
+ checkOnCondition( elem );
521
491
  }
522
492
 
523
493
  function getBinaryOp( cond ) {
@@ -543,18 +513,18 @@ function check( model ) {
543
513
  const finalType = artifact.type._artifact._effectiveType || artifact.type._artifact;
544
514
 
545
515
  if (artifact.items && !finalType.items) {
546
- warning('type-items-mismatch', [ artifact.type.location, artifact ],
547
- { type: artifact.type, prop: 'items' },
548
- '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' );
549
519
  }
550
520
  else if (artifact.elements && !finalType.elements) {
551
- warning('type-elements-mismatch', [ artifact.type.location, artifact ],
552
- { type: artifact.type, prop: 'elements' },
553
- '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' );
554
524
  }
555
525
  }
556
526
  if (artifact.items)
557
- checkTypeStructure(artifact.items);
527
+ checkTypeStructure( artifact.items );
558
528
  }
559
529
 
560
530
  /**
@@ -571,7 +541,7 @@ function check( model ) {
571
541
  if (element.$inferred !== 'include') {
572
542
  for (const include of def.includes) {
573
543
  if (include._artifact?.elements?.[name] !== undefined)
574
- checkElementOverride( element, include._artifact.elements[name]);
544
+ checkElementOverride( element, include._artifact.elements[name] );
575
545
  }
576
546
  }
577
547
  }
@@ -586,12 +556,12 @@ function check( model ) {
586
556
  const name = elem.name.id;
587
557
  // Position at type/struct, not name
588
558
  const loc = elem.type?.location || elem.elements?.[$location] || elem.location;
589
- error('ref-invalid-override', [ loc, elem ],
590
- { '#': prop, art: original._main, name });
559
+ error( 'ref-invalid-override', [ loc, elem ],
560
+ { '#': prop, art: original._main, name } );
591
561
  return false;
592
562
  }
593
563
  else if (original.elements &&
594
- !checkSubStructureOverride(elem, elem.elements, original.elements)) {
564
+ !checkSubStructureOverride( elem, elem.elements, original.elements )) {
595
565
  return false;
596
566
  }
597
567
  return true;
@@ -606,10 +576,10 @@ function check( model ) {
606
576
  const orig = originals[element];
607
577
  if (elem === undefined) {
608
578
  const loc = [ elements[$location], user ];
609
- error('ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element });
579
+ error( 'ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element } );
610
580
  return false; // only report once
611
581
  }
612
- else if (!checkElementOverride(elem, orig)) {
582
+ else if (!checkElementOverride( elem, orig )) {
613
583
  return false;
614
584
  }
615
585
  }
@@ -626,8 +596,8 @@ function check( model ) {
626
596
  * @returns {void}
627
597
  */
628
598
  function checkGenericExpression( xpr, user ) {
629
- checkExpressionNotVirtual(xpr, user);
630
- checkExpressionAssociationUsage(xpr, user, false);
599
+ checkExpressionNotVirtual( xpr, user );
600
+ checkExpressionAssociationUsage( xpr, user, false );
631
601
  if (xpr.op?.val === 'cast') {
632
602
  requireExplicitTypeInSqlCast( xpr, user );
633
603
  checkTypeArguments( xpr, user );
@@ -635,25 +605,24 @@ function check( model ) {
635
605
  }
636
606
 
637
607
  function checkExpressionNotVirtual( xpr, user ) {
638
- if (xpr._artifact && isVirtualElement(xpr._artifact))
639
- error('ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' });
608
+ if (xpr._artifact && isVirtualElement( xpr._artifact ))
609
+ error( 'ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' } );
640
610
  }
641
611
 
642
612
  function checkOnCondition( elem ) {
613
+ if (elem.$inferred === 'localized')
614
+ return; // ignore
615
+
643
616
  // TODO: Move to checkAssociation
644
617
  if (elem.on && !elem.on.$inferred) {
645
- visitExpression(elem.on, elem, (xpr, user) => {
646
- checkExpressionNotVirtual(xpr, user);
647
- checkExpressionAssociationUsage(xpr, user, true);
618
+ visitExpression( elem.on, elem, (xpr, user) => {
619
+ checkExpressionNotVirtual( xpr, user );
620
+ checkExpressionAssociationUsage( xpr, user, true );
648
621
  // Essential check. Dependency handling for `on` conditions must change if
649
622
  // this is allowed. See test3/Associations/Dependencies/.
650
623
  if (xpr._artifact?.$syntax === 'calc' && !xpr._artifact.value.stored?.val)
651
624
  error( 'ref-unexpected-calculated', [ xpr.location, user ], { '#': 'on' } );
652
- });
653
- if (isDollarSelfOrProjectionOperand(elem.on)) {
654
- // Bare $self usages are not allowed and don't work in A2J.
655
- error('expr-missing-comparison', [ elem.on.location, elem ], { id: '$self', op: '=' } );
656
- }
625
+ } );
657
626
  }
658
627
  }
659
628
 
@@ -666,37 +635,41 @@ function check( model ) {
666
635
  requireExplicitTypeInSqlCast( elem.value, elem );
667
636
  checkTypeArguments( elem.value, elem );
668
637
  }
669
- visitSubExpression(elem.value, elem, (xpr) => {
638
+ visitSubExpression( elem.value, elem, (xpr) => {
670
639
  checkGenericExpression( xpr, elem );
671
- });
640
+ } );
672
641
  }
673
642
 
674
643
  function checkCalculatedElementValue( elem ) {
675
- visitExpression(elem.value, elem, (xpr, user) => {
676
- 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) {
677
650
  const sourceLoc = xpr.path?.[xpr.path.length - 1].location || xpr.location;
678
- checkExpressionNotVirtual(xpr, user);
651
+ checkExpressionNotVirtual( xpr, user );
679
652
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
680
653
  // And users can't change structured to non-structured elements.
681
654
  if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
682
- error('ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
655
+ error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
683
656
  }
684
657
  else if (xpr._artifact.target !== undefined) {
685
- const variant = isComposition(model, xpr._artifact) ? 'expr-comp' : 'expr';
686
- error('ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant });
658
+ const variant = isComposition( model, xpr._artifact ) ? 'expr-comp' : 'expr';
659
+ error( 'ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant } );
687
660
  }
688
661
  else if (xpr._artifact.localized?.val && elem.value.stored?.val) {
689
- error('ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' });
662
+ error( 'ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' } );
690
663
  }
691
664
  }
692
- });
665
+ } );
693
666
  // Calculated elements must not refer to keys, because that may lead to another
694
667
  // key in an SQL view, which is missing in OData (for on-read).
695
668
  // Following associations does not lead to this issue.
696
- if (elem.value.path && isKeyElement(elem.value._artifact) &&
697
- !followsAnAssociation(elem.value.path)) {
698
- error('ref-unexpected-key', [ elem.value.location, elem ], {},
699
- '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' );
700
673
  }
701
674
  }
702
675
 
@@ -758,21 +731,15 @@ function check( model ) {
758
731
  // Only check associations and $self if this is not a backlink-like
759
732
  // expression (a comparison of $self with an assoc).
760
733
  // We don't check token-stream-like 'xpr's.
761
- const isNotSelfComparison = xpr.op?.val !== 'xpr' &&
762
- !isBinaryDollarSelfComparisonWithAssoc(xpr);
763
- const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
764
- 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 );
765
737
 
766
738
  if (isNotSelfComparison) {
739
+ const op = getBinaryOp( xpr );
767
740
  for (const arg of args) {
768
- if (op?.val !== '=' && isDollarSelfOrProjectionOperand(arg)) {
769
- // `nary` operators don't have a "good" location; use $self in that case.
770
- const loc = (op?.location.endLine ? op : arg).location;
771
- error('expr-invalid-operator', [ loc, user ], { op: '=', id: '$self' });
772
- }
773
- else {
774
- checkExpressionIsNotAssocOrSelf(arg, user, allowAssocTail);
775
- }
741
+ if (arg && !(op?.val !== '=' && isDollarSelfOrProjectionOperand( arg )))
742
+ checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail );
776
743
  }
777
744
  }
778
745
  }
@@ -782,18 +749,13 @@ function check( model ) {
782
749
  // Only if path is not approved exists path (that is non-query position)
783
750
  if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
784
751
  if (arg.$expected === 'exists') {
785
- const variant = isComposition(model, arg._artifact) ? 'expr-comp' : 'expr';
786
- error('ref-unexpected-assoc', [ arg.location, user ], { '#': variant });
752
+ const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
753
+ error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
787
754
  }
788
755
  }
789
- else if (!allowAssocTail && isAssociationOperand(arg)) {
790
- const variant = isComposition(model, arg._artifact) ? 'expr-comp' : 'expr';
791
- error('ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
792
- }
793
-
794
- if (isDollarSelfOrProjectionOperand(arg)) {
795
- error(null, [ arg.location, user ], { id: arg.path[0].id },
796
- '$(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 } );
797
759
  }
798
760
  }
799
761
 
@@ -827,13 +789,17 @@ function check( model ) {
827
789
  // One argument must be "$self" and the other an assoc
828
790
  if (xpr.op.val === '=' && xpr.args.length === 2) {
829
791
  // Tree-ish expression from the compiler (not augmented)
830
- return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[1]) ||
831
- 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] ));
832
796
  }
833
797
  else if (xpr.args.length === 3 && xpr.args[1].val === '=') {
834
798
  // Tree-ish expression from the compiler (not augmented)
835
- return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[2]) ||
836
- 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] ));
837
803
  }
838
804
 
839
805
  // Nothing else qualifies
@@ -858,7 +824,7 @@ function check( model ) {
858
824
  let fromArtifact = null;
859
825
  let pathStepsFound = 0;
860
826
  for (let i = anno.name.path.length; i > 0; i--) {
861
- 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( '.' );
862
828
  if (model.vocabularies[absoluteName]) {
863
829
  fromArtifact = model.vocabularies[absoluteName];
864
830
  pathStepsFound = i;
@@ -871,8 +837,8 @@ function check( model ) {
871
837
  return;
872
838
  }
873
839
 
874
- const { artifact, endOfPath } = resolvePathFrom(anno.name.path.slice(pathStepsFound),
875
- fromArtifact);
840
+ const { artifact, endOfPath } = resolvePathFrom( anno.name.path.slice( pathStepsFound ),
841
+ fromArtifact );
876
842
 
877
843
  // Check what we actually want to check
878
844
  checkAnnotationAssignment( anno, artifact, endOfPath, art );
@@ -892,33 +858,33 @@ function check( model ) {
892
858
 
893
859
  // Element must exist in annotation
894
860
  if (!elementDecl) {
895
- warning(null, [ anno.location || anno.name.location, art ],
896
- { name: pathName(anno.name.path), anno: annoDecl.name.absolute },
897
- '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)' );
898
864
  return;
899
865
  }
900
866
 
901
867
  // Sanity checks
902
868
  if (!elementDecl._effectiveType)
903
- 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 ) }`);
904
870
 
905
871
 
906
872
  // Must have literal or path unless it is a boolean
907
- if (!anno.literal && !anno.path && getFinalTypeNameOf(elementDecl) !== 'cds.Boolean') {
873
+ if (!anno.literal && !anno.path && getFinalTypeNameOf( elementDecl ) !== 'cds.Boolean') {
908
874
  if (elementDecl.type?._artifact.name.absolute) {
909
- warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
910
- { '#': 'type', type: elementDecl.type._artifact });
875
+ warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
876
+ { '#': 'type', type: elementDecl.type._artifact } );
911
877
  }
912
878
  else {
913
- warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
914
- { '#': 'std', anno: anno.name.absolute });
879
+ warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
880
+ { '#': 'std', anno: anno.name.absolute } );
915
881
  }
916
882
 
917
883
  return;
918
884
  }
919
885
 
920
886
  // Value must be assignable to type
921
- checkValueAssignableTo(anno, anno, elementDecl, art);
887
+ checkValueAssignableTo( anno, anno, elementDecl, art );
922
888
  }
923
889
 
924
890
  // Check that annotation assignment 'value' (having 'path or 'literal' and
@@ -937,12 +903,12 @@ function check( model ) {
937
903
  if (elementDecl._effectiveType.items) {
938
904
  // Make sure we have an array value
939
905
  if (value.literal !== 'array') {
940
- 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)' );
941
907
  return;
942
908
  }
943
909
  // Check each element
944
910
  for (const valueItem of value.val)
945
- checkValueAssignableTo(value, valueItem, elementDecl._effectiveType.items, art);
911
+ checkValueAssignableTo( value, valueItem, elementDecl._effectiveType.items, art );
946
912
 
947
913
  return;
948
914
  }
@@ -950,7 +916,7 @@ function check( model ) {
950
916
  // Struct expected (can only happen within arrays)?
951
917
  if (elementDecl._effectiveType.elements) {
952
918
  if (value.literal !== 'struct') {
953
- 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)' );
954
920
  return;
955
921
  }
956
922
  // FIXME: Should check each element
@@ -959,44 +925,44 @@ function check( model ) {
959
925
 
960
926
  // Handle each (primitive) expected element type separately
961
927
  // TODO: Don't rely on name; use actual type
962
- const type = getFinalTypeNameOf(elementDecl);
963
- if (builtins.isStringTypeName(type)) {
928
+ const type = getFinalTypeNameOf( elementDecl );
929
+ if (builtins.isStringTypeName( type )) {
964
930
  if (value.literal !== 'string' && value.literal !== 'enum' &&
965
931
  !elementDecl._effectiveType.enum) {
966
- warning(null, loc, { type, anno },
967
- '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)' );
968
934
  }
969
935
  }
970
- else if (builtins.isBinaryTypeName(type)) {
936
+ else if (builtins.isBinaryTypeName( type )) {
971
937
  if (value.literal !== 'string' && value.literal !== 'x') {
972
- warning(null, loc, { type, anno },
973
- '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)' );
974
940
  }
975
941
  }
976
- else if (builtins.isNumericTypeName(type)) {
942
+ else if (builtins.isNumericTypeName( type )) {
977
943
  if (value.literal !== 'number' && value.literal !== 'enum' &&
978
944
  !elementDecl._effectiveType.enum) {
979
- warning(null, loc, { type, anno },
980
- '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)' );
981
947
  }
982
948
  }
983
- else if (builtins.isDateOrTimeTypeName(type)) {
949
+ else if (builtins.isDateOrTimeTypeName( type )) {
984
950
  if (value.literal !== 'date' && value.literal !== 'time' &&
985
951
  value.literal !== 'timestamp' && value.literal !== 'string') {
986
- warning(null, loc, { type, anno },
987
- // eslint-disable-next-line max-len
988
- '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)' );
989
955
  }
990
956
  }
991
- else if (builtins.isBooleanTypeName(type)) {
957
+ else if (builtins.isBooleanTypeName( type )) {
992
958
  if (value.literal && value.literal !== 'boolean') {
993
- warning(null, loc, { type, anno },
994
- '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)' );
995
961
  }
996
962
  }
997
- else if (builtins.isRelationTypeName(type) || builtins.isGeoTypeName(type)) {
998
- warning(null, loc, { type, anno },
999
- '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)' );
1000
966
  }
1001
967
  else if (!elementDecl._effectiveType.enum) {
1002
968
  throw new CompilerAssertion(`Unknown primitive type name: ${ type }`);
@@ -1009,22 +975,22 @@ function check( model ) {
1009
975
  // Enum symbol provided and expected
1010
976
  if (!expectedEnum[value.sym.id]) {
1011
977
  // ... but no such constant
1012
- 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)' );
1013
979
  }
1014
980
  }
1015
981
  else {
1016
982
  // Enum symbol provided but not expected
1017
- warning(null, loc, { id: `#${ value.sym.id }`, type, anno },
1018
- '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)' );
1019
985
  }
1020
986
  }
1021
987
  else if (expectedEnum) {
1022
988
  // Enum symbol not provided but expected
1023
- const hasValidValue = Object.keys(expectedEnum)
1024
- .some(symbol => getEnumValue(expectedEnum[symbol]) === value.val);
989
+ const hasValidValue = Object.keys( expectedEnum )
990
+ .some( symbol => getEnumValue( expectedEnum[symbol] ) === value.val );
1025
991
  if (!hasValidValue) {
1026
992
  // ... and none of the valid enum symbols matches the value
1027
- 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)' );
1028
994
  }
1029
995
  }
1030
996
  }
@@ -1059,7 +1025,7 @@ function check( model ) {
1059
1025
  // Continue search with next path step
1060
1026
  const nextStepEnv = (from._effectiveType || from).artifacts ||
1061
1027
  from._effectiveType?.elements || [];
1062
- return resolvePathFrom(path.slice(1), nextStepEnv[path[0].id], result);
1028
+ return resolvePathFrom( path.slice(1), nextStepEnv[path[0].id], result );
1063
1029
  }
1064
1030
 
1065
1031
  // TODO: remove
@@ -1094,7 +1060,7 @@ function checkSapCommonTextsAspects( model ) {
1094
1060
  elemref: 'locale',
1095
1061
  type: 'cds.String',
1096
1062
  othertype: 'sap.common.Locale',
1097
- });
1063
+ } );
1098
1064
  }
1099
1065
  }
1100
1066
  }
@@ -1129,6 +1095,9 @@ function checkSapCommonLocale( model ) {
1129
1095
  * @param {(xpr: any, user: any, parentExpr: any) => void} callback
1130
1096
  */
1131
1097
  function visitExpression( xpr, user, callback ) {
1098
+ if (!xpr)
1099
+ return; // e.g. parse error
1100
+
1132
1101
  callback( xpr, user, null );
1133
1102
  visitSubExpression( xpr, user, callback );
1134
1103
  }
@@ -1142,12 +1111,14 @@ function visitExpression( xpr, user, callback ) {
1142
1111
  */
1143
1112
  function visitSubExpression( xpr, user, callback ) {
1144
1113
  if (xpr.args) {
1145
- 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 );
1146
1115
  // Check for illegal argument usage within the expression
1147
1116
  for (const arg of args) {
1148
- callback( arg, user, xpr.args );
1149
- // Recursively traverse the argument expression
1150
- 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
+ }
1151
1122
  }
1152
1123
  }
1153
1124
 
@@ -1155,7 +1126,7 @@ function visitSubExpression( xpr, user, callback ) {
1155
1126
  for (const arg of xpr.path) {
1156
1127
  if (arg.where) {
1157
1128
  callback( arg.where, user, arg );
1158
- visitSubExpression(arg.where, user, callback);
1129
+ visitSubExpression( arg.where, user, callback );
1159
1130
  }
1160
1131
  }
1161
1132
  }