@sap/cds-compiler 3.6.2 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +3 -0
  3. package/bin/cdsc.js +12 -5
  4. package/doc/CHANGELOG_ARCHIVE.md +6 -6
  5. package/doc/CHANGELOG_BETA.md +35 -2
  6. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  7. package/doc/DeprecatedOptions_v2.md +1 -1
  8. package/doc/NameResolution.md +1 -1
  9. package/lib/api/main.js +63 -23
  10. package/lib/api/options.js +1 -0
  11. package/lib/api/validate.js +5 -0
  12. package/lib/base/dictionaries.js +15 -3
  13. package/lib/base/keywords.js +2 -0
  14. package/lib/base/message-registry.js +120 -34
  15. package/lib/base/messages.js +51 -27
  16. package/lib/base/model.js +4 -2
  17. package/lib/base/shuffle.js +2 -1
  18. package/lib/checks/arrayOfs.js +1 -1
  19. package/lib/checks/defaultValues.js +1 -1
  20. package/lib/checks/elements.js +29 -1
  21. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +10 -6
  22. package/lib/checks/invalidTarget.js +1 -1
  23. package/lib/checks/nonexpandableStructured.js +1 -1
  24. package/lib/checks/onConditions.js +15 -9
  25. package/lib/checks/sql-snippets.js +2 -2
  26. package/lib/checks/types.js +5 -1
  27. package/lib/checks/validator.js +7 -3
  28. package/lib/compiler/assert-consistency.js +42 -26
  29. package/lib/compiler/base.js +50 -4
  30. package/lib/compiler/builtins.js +17 -8
  31. package/lib/compiler/checks.js +241 -246
  32. package/lib/compiler/define.js +113 -146
  33. package/lib/compiler/extend.js +889 -383
  34. package/lib/compiler/finalize-parse-cdl.js +5 -58
  35. package/lib/compiler/index.js +1 -1
  36. package/lib/compiler/kick-start.js +7 -8
  37. package/lib/compiler/populate.js +297 -293
  38. package/lib/compiler/propagator.js +27 -18
  39. package/lib/compiler/resolve.js +146 -463
  40. package/lib/compiler/shared.js +36 -79
  41. package/lib/compiler/tweak-assocs.js +30 -28
  42. package/lib/compiler/utils.js +31 -5
  43. package/lib/edm/annotations/genericTranslation.js +131 -59
  44. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  45. package/lib/edm/csn2edm.js +22 -5
  46. package/lib/edm/edm.js +6 -4
  47. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  48. package/lib/edm/edmPreprocessor.js +42 -26
  49. package/lib/gen/Dictionary.json +38 -2
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +3 -1
  52. package/lib/gen/languageLexer.js +1 -1
  53. package/lib/gen/languageParser.js +4828 -4472
  54. package/lib/inspect/inspectPropagation.js +20 -34
  55. package/lib/json/from-csn.js +140 -44
  56. package/lib/json/to-csn.js +114 -122
  57. package/lib/language/errorStrategy.js +2 -0
  58. package/lib/language/genericAntlrParser.js +156 -36
  59. package/lib/language/language.g4 +100 -58
  60. package/lib/language/textUtils.js +13 -0
  61. package/lib/main.d.ts +43 -3
  62. package/lib/main.js +4 -2
  63. package/lib/model/csnRefs.js +15 -3
  64. package/lib/model/csnUtils.js +12 -74
  65. package/lib/model/revealInternalProperties.js +4 -2
  66. package/lib/modelCompare/compare.js +2 -1
  67. package/lib/optionProcessor.js +3 -0
  68. package/lib/render/manageConstraints.js +5 -2
  69. package/lib/render/toCdl.js +216 -104
  70. package/lib/render/toHdbcds.js +2 -9
  71. package/lib/render/toRename.js +14 -51
  72. package/lib/render/toSql.js +4 -3
  73. package/lib/render/utils/common.js +9 -5
  74. package/lib/transform/braceExpression.js +6 -0
  75. package/lib/transform/db/assertUnique.js +2 -1
  76. package/lib/transform/db/expansion.js +2 -0
  77. package/lib/transform/db/flattening.js +37 -36
  78. package/lib/transform/db/rewriteCalculatedElements.js +600 -0
  79. package/lib/transform/db/transformExists.js +4 -0
  80. package/lib/transform/db/views.js +40 -37
  81. package/lib/transform/forOdataNew.js +20 -15
  82. package/lib/transform/forRelationalDB.js +58 -41
  83. package/lib/transform/odata/typesExposure.js +50 -15
  84. package/lib/transform/parseExpr.js +16 -8
  85. package/lib/transform/transformUtilsNew.js +42 -14
  86. package/lib/transform/translateAssocsToJoins.js +60 -37
  87. package/lib/transform/universalCsn/coreComputed.js +15 -7
  88. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  89. package/package.json +2 -1
@@ -26,15 +26,17 @@ function check( model ) { // = XSN
26
26
  const {
27
27
  error, warning, message, info,
28
28
  } = model.$messageFunctions;
29
- forEachDefinition( model, checkArtifact );
30
- forEachGeneric( model, 'vocabularies', checkAnnotationDefinition );
29
+
31
30
  checkSapCommonLocale( model );
32
31
  checkSapCommonTextsAspects( model );
32
+
33
+ forEachDefinition( model, checkDefinition );
34
+ forEachGeneric( model, 'vocabularies', checkAnnotationDefinition );
33
35
  return;
34
36
 
35
- function checkArtifact( art ) {
37
+ function checkDefinition( art ) {
36
38
  checkGenericConstruct( art );
37
- forEachGeneric( art, 'elements', checkElement );
39
+ forEachMember( art, member => checkMember(member) );
38
40
  if (art.$queries)
39
41
  art.$queries.forEach( checkQuery );
40
42
  }
@@ -59,36 +61,50 @@ function check( model ) { // = XSN
59
61
  if (art.kind === 'enum')
60
62
  checkEnum( art );
61
63
  checkEnumType( art );
64
+ }
62
65
 
63
- forEachMember( art, checkGenericConstruct );
66
+ function checkMember( member, parentProps = { key: false, virtual: false } ) {
67
+ // To avoid "bubble-up" checks, store required parent properties.
68
+ if (member.key?.val === true)
69
+ parentProps.key = member.key;
70
+ if (member.virtual?.val === true)
71
+ parentProps.virtual = member.virtual;
72
+
73
+ checkGenericConstruct(member);
74
+
75
+ if (member.kind === 'element')
76
+ checkElement( member, parentProps );
77
+
78
+ forEachMember( member, m => checkMember(m, parentProps) );
64
79
  }
65
80
 
66
- function checkElement( elem ) {
67
- checkLocalizedSubElement(elem);
68
- if (elem.key?.val) {
69
- if (elem.virtual?.val) {
70
- error('def-unexpected-key', [ elem.key.location, elem ],
71
- { '#': 'virtual', name: elem.name.element, prop: 'key' });
72
- }
73
- checkForUnmanagedAssociationsAsKey( elem, elem.key );
81
+ function checkVirtualKey( elem, parentProps ) {
82
+ const isKey = parentProps.key || (elem.key?.val && elem.key);
83
+ const isVirtual = parentProps.virtual?.val || (elem.virtual?.val && elem.virtual);
84
+ if (isKey && isVirtual) {
85
+ error('def-unexpected-key', [ isKey.location, elem ],
86
+ { '#': 'virtual', name: elem.name.element, prop: 'key' });
74
87
  }
75
- checkAssociation( elem );
88
+ }
89
+
90
+ function checkElement( elem, parentProps ) {
91
+ checkLocalizedSubElement(elem);
92
+ checkVirtualKey(elem, parentProps);
93
+ checkForUnmanagedAssociationsAsKey( elem, parentProps );
76
94
  checkLocalizedElement( elem );
77
- if (elem.on && !elem.on.$inferred)
78
- checkExpression(elem.on, true);
95
+ checkAssociation( elem );
79
96
 
80
97
  if (elem.value) {
81
- if (elem.$syntax === 'calc')
82
- checkCalculatedElement( elem );
83
- else
84
- checkExpression( elem.value );
98
+ if (elem._main.query)
99
+ checkSelectItemValue(elem);
100
+ else if (elem.$syntax === 'calc')
101
+ checkCalculatedElementValue( elem );
85
102
  }
86
103
 
87
104
  checkCardinality(elem); // TODO: also for assoc types
88
-
89
- forEachGeneric( elem, 'elements', checkElement );
90
105
  }
91
106
 
107
+
92
108
  function checkName( construct ) { // TODO: move to define.js
93
109
  if (model.options.$skipNameCheck)
94
110
  return;
@@ -125,62 +141,30 @@ function check( model ) { // = XSN
125
141
  }
126
142
  }
127
143
 
128
- function checkCalculatedElement( elem ) {
129
- if (elem.value.path) {
130
- checkExpressionsInPaths(elem.value);
131
-
132
- const loc = [ elem.value.location, elem ];
133
- if (isVirtualElement(elem.value._artifact))
134
- error('ref-unexpected-virtual', loc, { '#': 'expr' });
135
- else if (isStructuredElement(elem.value._artifact))
136
- error('ref-unexpected-structured', loc, { '#': 'expr' } );
137
- else if (elem.value._artifact?.target !== undefined)
138
- error('ref-unexpected-assoc', loc, { '#': 'expr' });
139
- }
140
- else {
141
- // TODO: The checks above should also be run for each path in expressions.
142
- checkExpression( elem.value );
143
- }
144
- }
145
-
146
144
  function checkQuery( query ) {
147
145
  checkNoUnmanagedAssocsInGroupByOrderBy( query );
148
146
  // TODO: check too simple (just one source), as most of those in this file
149
147
  // Check expressions in the various places where they may occur
150
148
  if (query.from)
151
- checkExpressionsInPaths(query.from);
149
+ visitSubExpression(query.from, query, checkGenericExpression);
150
+
151
+ if (query.where)
152
+ visitExpression(query.where, query, checkGenericExpression);
152
153
 
153
- if (query.where) {
154
- checkExpression(query.where);
155
- checkExpressionsInPaths(query.where);
156
- }
157
154
  if (query.groupBy) {
158
- for (const groupByEntry of query.groupBy) {
159
- checkExpression(groupByEntry);
160
- checkExpressionsInPaths(groupByEntry);
161
- }
162
- }
163
- if (query.having) {
164
- checkExpression(query.having);
165
- checkExpressionsInPaths(query.having);
155
+ for (const groupByEntry of query.groupBy)
156
+ visitExpression(groupByEntry, query, checkGenericExpression);
166
157
  }
158
+ if (query.having)
159
+ visitExpression(query.having, query, checkGenericExpression);
160
+
167
161
  if (query.orderBy) {
168
- for (const orderByEntry of query.orderBy) {
169
- checkExpression(orderByEntry);
170
- checkExpressionsInPaths(orderByEntry);
171
- }
162
+ for (const orderByEntry of query.orderBy)
163
+ visitExpression(orderByEntry, query, checkGenericExpression);
172
164
  }
173
165
  if (query.mixin) {
174
- for (const mixinName in query.mixin) {
175
- if (query.mixin[mixinName].on)
176
- checkExpression(query.mixin[mixinName].on, true);
177
- }
178
- }
179
- if (query.elements) {
180
- for (const elemName in query.elements) {
181
- checkStructureCasting(query.elements[elemName]);
182
- checkExpressionsInPaths(query.elements[elemName].value);
183
- }
166
+ for (const mixinName in query.mixin)
167
+ checkAssociation(query.mixin[mixinName]);
184
168
  }
185
169
  }
186
170
 
@@ -358,25 +342,27 @@ function check( model ) { // = XSN
358
342
  * Check that a primary key element is not an unmanaged association or
359
343
  * contains unmanaged associations
360
344
  *
345
+ * TODO: ease check for subelements: using unmanaged assocs is OK there, as
346
+ * long as the whole key is "closed", i.e., no ref in ON refers to element
347
+ * outside.
348
+ *
361
349
  * @param {any} element Element to check recursively
362
350
  */
363
- function checkForUnmanagedAssociationsAsKey( element, keyObj ) {
351
+ function checkForUnmanagedAssociationsAsKey( element, parentProps ) {
352
+ if (!parentProps.key?.val && !element.key?.val)
353
+ return;
364
354
  if (element.targetAspect) {
365
355
  // TODO: bad location / message
366
- message('composition-as-key', [ keyObj.location, element ], {},
356
+ message('composition-as-key', [ parentProps.key.location, element ], {},
367
357
  // TODO: give semantics when error downgraded
368
358
  'Managed compositions can\'t be used as primary key');
369
359
  }
370
360
  else if (element.on) {
371
361
  // TODO: bad location / message
372
- message('unmanaged-as-key', [ keyObj.location, element ], {},
362
+ message('unmanaged-as-key', [ parentProps.key.location, element ], {},
373
363
  // TODO: give semantics when error downgraded
374
364
  'Unmanaged associations can\'t be used as primary key');
375
365
  }
376
- // TODO: ease check for subelements: using unmanaged assocs is OK there, as
377
- // long as the whole key is "closed", i.e., no ref in ON refers to element
378
- // outside.
379
- forEachGeneric( element, 'elements', e => checkForUnmanagedAssociationsAsKey( e, keyObj ) );
380
366
  }
381
367
 
382
368
  // Check that min and max cardinalities of 'elem' in 'art' have legal values
@@ -438,27 +424,6 @@ function check( model ) { // = XSN
438
424
  });
439
425
  }
440
426
 
441
- // TODO: yes, a check similar to this could make it into the compiler)
442
- // Check that a structured element ist not casted to a different type
443
- function checkStructureCasting( elem ) {
444
- if (elem.type && !elem.type.$inferred) {
445
- const loc = elem.type.location || elem.location;
446
-
447
- if (elem._effectiveType && elem._effectiveType.elements) {
448
- error('type-cast-to-structured', [ loc, elem ], {},
449
- 'Can\'t cast to structured element');
450
- }
451
- else if (elem.value && elem.value._artifact && elem.value._artifact._effectiveType &&
452
- elem.value._artifact._effectiveType.elements) {
453
- error('type-cast-structured', [ loc, elem ], {},
454
- 'Structured elements can\'t be cast to a different type');
455
- }
456
- }
457
- if (elem.value && Array.isArray( elem.value.args)) { // TODO named args?
458
- elem.value.args.forEach(checkStructureCasting);
459
- }
460
- }
461
-
462
427
  // TODO: make this part of the name resolution in the compiler
463
428
  // Check that queries in 'art' do not contain unmanaged associations in GROUP BY or ORDER BY
464
429
  function checkNoUnmanagedAssocsInGroupByOrderBy( query ) {
@@ -481,22 +446,6 @@ function check( model ) { // = XSN
481
446
  }
482
447
  }
483
448
 
484
- // Traverses 'node' recursively and applies 'checkExpression' to all expressions
485
- // found within paths (e.g. filters, parameters, ...)
486
- function checkExpressionsInPaths( node ) {
487
- foreachPath(node, (path) => {
488
- for (const pathStep of path) {
489
- if (pathStep.where)
490
- checkExpression(pathStep.where);
491
-
492
- // FIXME: I can't actually think of a way to make this check fail, because
493
- // params are limited to actual values and params
494
- if (pathStep.args)
495
- checkExpression(pathStep.args);
496
- }
497
- });
498
- }
499
-
500
449
  function checkAssociation( elem ) {
501
450
  // TODO: yes, a check similar to this could make it into the compiler)
502
451
  // when virtual element is part of association
@@ -505,80 +454,20 @@ function check( model ) { // = XSN
505
454
  const key = elem.foreignKeys[k].targetElement;
506
455
  if (key && isVirtualElement(key._artifact))
507
456
  error('ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' });
457
+ else if (key._artifact?.$syntax === 'calc')
458
+ error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );
508
459
  }
509
460
  }
510
- if (elem.on && !elem.on.$inferred)
511
- checkAssociationCondition(elem, elem.on);
512
- }
513
461
 
514
- function checkAssociationCondition( elem, onCond ) {
515
- if (Array.isArray(onCond)) // condition in brackets results an array
516
- onCond.forEach(Cond => checkAssociationCondition(elem, Cond));
517
- else
518
- checkAssociationConditionArgs(elem, onCond.args, getBinaryOp( onCond ));
462
+ checkOnCondition(elem);
519
463
  }
520
464
 
521
465
  function getBinaryOp( cond ) {
522
466
  const { op, args } = cond;
523
- return op?.val === 'ixpr' && args.length === 3 && args[1].literal === 'token' &&
467
+ return op?.val === 'ixpr' && args?.length === 3 && args[1].literal === 'token' &&
524
468
  args[1] || op;
525
469
  }
526
470
 
527
- function checkAssociationConditionArgs( elem, args, op ) {
528
- if (args)
529
- args.forEach(Arg => checkAssociationOnCondArg(elem, Arg, op));
530
- }
531
-
532
- function checkAssociationOnCondArg( elem, arg, op ) {
533
- if (Array.isArray(arg)) {
534
- arg.forEach(Arg => checkAssociationCondition(elem, Arg));
535
- }
536
- else {
537
- checkAssociationCondition(elem, arg);
538
- singleCheckUnmanagedAssocCondArgumentNoFollowUnmanagedAssoc(elem, arg, op);
539
- }
540
- }
541
-
542
- // TODO: make it part of the name resolution in the compiler to check whether
543
- // associations can be followed (in the ON condition)
544
- //
545
- // TODO: this function must be completely reworked, probably even before
546
- // integration into name resolution - did the first step.
547
- // It is also incomplete, as associations in structures are not checked.
548
- // Additionally, `$self.assoc` references are also not found.
549
- function singleCheckUnmanagedAssocCondArgumentNoFollowUnmanagedAssoc( elem, arg, op ) {
550
- if (!arg.path)
551
- return;
552
- const path0 = arg.path[0];
553
- if (!path0)
554
- return;
555
- if (path0.id === '$self' && arg.path.length === 1) { // $self (backlink) checks
556
- checkAssociationArgumentStartingWithSelf( op, elem );
557
- return;
558
- }
559
- const argTarget = path0._artifact;
560
- if (!argTarget) // not resolved
561
- return;
562
- // the check is valid for unmanaged associations
563
-
564
- // TODO clarify if the full resolved path to the target field should
565
- // consist of managed associations or just the first
566
- if (argTarget.on) {
567
- const same = path0._artifact === elem;
568
- if (!same) {
569
- error(null, [ path0.location, elem ], {},
570
- 'Unmanaged association condition can\'t follow another unmanaged association');
571
- }
572
- }
573
- }
574
-
575
- function checkAssociationArgumentStartingWithSelf( op, elem ) {
576
- if (op?.val === 'xpr') // no check for xpr, would require re-structuring
577
- return;
578
- if (op && op.val !== '=')
579
- error(null, [ op.location, elem ], {}, '$self comparison is only allowed with \'=\'');
580
- }
581
-
582
471
  // A function like this could be part of the compiler
583
472
  /**
584
473
  * Check that the given type has no conflicts between its `type` property
@@ -612,19 +501,92 @@ function check( model ) { // = XSN
612
501
  // Former checkExpressions.js ----------------------------------------------
613
502
 
614
503
  /**
615
- * Check an expression (or condition) for semantic validity
504
+ * Check a generic expression (or condition) for semantic validity.
616
505
  *
617
506
  * @param {any} xpr The expression to check
618
- * @param {Boolean} allowAssocTail
507
+ * @param {XSN.Artifact} user User for semantic location
619
508
  * @returns {void}
620
509
  */
621
- function checkExpression( xpr, allowAssocTail = false ) {
622
- // Since the checks for tree-like and token-stream expressions differ,
623
- // check here what kind of expression we are looking at
624
- if (xpr.op?.val === 'xpr')
625
- return checkTokenStreamExpression(xpr, allowAssocTail);
626
- return checkTreeLikeExpression(xpr, allowAssocTail);
510
+ function checkGenericExpression( xpr, user ) {
511
+ checkExpressionNotVirtual(xpr, user);
512
+ checkExpressionAssociationUsage(xpr, user, false);
627
513
  }
514
+
515
+ function checkExpressionNotVirtual( xpr, user ) {
516
+ if (xpr._artifact && isVirtualElement(xpr._artifact))
517
+ error('ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' });
518
+ }
519
+
520
+ function checkOnCondition( elem ) {
521
+ // TODO: Move to checkAssociation
522
+ if (elem.on && !elem.on.$inferred) {
523
+ visitExpression(elem.on, elem, (xpr, user) => {
524
+ checkExpressionNotVirtual(xpr, user);
525
+ checkExpressionAssociationUsage(xpr, user, true);
526
+ if (xpr._artifact?.$syntax === 'calc')
527
+ error( 'ref-unexpected-calculated', [ xpr.location, user ], { '#': 'on' } );
528
+ });
529
+ if (isDollarSelfOrProjectionOperand(elem.on)) {
530
+ // Bare $self usages are not allowed and don't work in A2J.
531
+ error('expr-missing-comparison', [ elem.on.location, elem ], { id: '$self', op: '=' } );
532
+ }
533
+ }
534
+ }
535
+
536
+ function checkSelectItemValue( elem ) {
537
+ checkExpressionAssociationUsage(elem.value, elem, false);
538
+
539
+ visitSubExpression(elem.value, elem, (xpr) => {
540
+ checkExpressionNotVirtual(xpr, elem);
541
+ checkExpressionAssociationUsage(xpr, elem, false);
542
+ });
543
+ }
544
+
545
+ function checkCalculatedElementValue( elem ) {
546
+ visitExpression(elem.value, elem, (xpr, user) => {
547
+ if (xpr._artifact) { // we only need to check artifact references
548
+ checkExpressionNotVirtual(xpr, user);
549
+ if (isStructuredElement(xpr._artifact))
550
+ error('ref-unexpected-structured', [ xpr.location, elem ], { '#': 'expr' } );
551
+ else if (xpr._artifact.target !== undefined)
552
+ error('ref-unexpected-assoc', [ xpr.location, elem ], { '#': 'expr' });
553
+ }
554
+ });
555
+ // Calc elements must not refer to keys, because that may lead to another key
556
+ // in an SQL view, which is missing in OData.
557
+ // Following associations does not lead to this issue.
558
+ if (elem.value.path && isKeyElement(elem.value._artifact) &&
559
+ !followsAnAssociation(elem.value.path)) {
560
+ error('ref-unexpected-key', [ elem.value.location, elem ], {},
561
+ 'Calculated elements can\'t refer directly to key elements');
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Returns true if any of the path steps follows an association.
567
+ *
568
+ * @param path
569
+ * @return {boolean}
570
+ */
571
+ function followsAnAssociation( path ) {
572
+ for (const step of path) {
573
+ if (step._artifact?.target)
574
+ return true;
575
+ }
576
+ return false;
577
+ }
578
+
579
+ function isKeyElement( elem ) {
580
+ let parent = elem;
581
+ while (parent) {
582
+ if (parent.key?.val === true)
583
+ return true;
584
+ parent = parent._parent;
585
+ }
586
+ return false;
587
+ }
588
+
589
+
628
590
  /**
629
591
  * Check whether the supplied argument is a virtual element
630
592
  *
@@ -650,60 +612,57 @@ function check( model ) { // = XSN
650
612
  return !!(elem?._effectiveType || elem)?.elements;
651
613
  }
652
614
 
653
- /**
654
- * Check a token-stream expression for semantic validity
655
- *
656
- * @param {any} xpr The expression to check
657
- * @returns {void}
658
- */
659
- function checkTokenStreamExpression( xpr, allowAssocTail ) {
660
- const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args || {});
661
- // Check for illegal argument usage within the expression
662
- for (const arg of args) {
663
- if (isVirtualElement(arg._artifact || arg))
664
- error('ref-unexpected-virtual', arg.location, { '#': 'expr' });
665
-
666
- // Recursively traverse the argument expression
667
- checkTokenStreamExpression(arg, allowAssocTail);
668
- }
669
- }
670
-
671
615
  /**
672
616
  * Check a tree-like expression for semantic validity
673
617
  *
674
618
  * @param {any} xpr The expression to check
619
+ * @param {XSN.Artifact} user
620
+ * @param {boolean} allowAssocTail
675
621
  * @returns {void}
676
622
  */
677
- function checkTreeLikeExpression( xpr, allowAssocTail ) {
678
- // No further checks regarding associations and $self required if this is a
679
- // backlink-like expression (a comparison of $self with an assoc)
680
- if (isBinaryDollarSelfComparisonWithAssoc(xpr))
623
+ function checkExpressionAssociationUsage( xpr, user, allowAssocTail ) {
624
+ if (!xpr.args)
681
625
  return;
682
626
 
683
- // Check for illegal argument usage within the expression
684
- for (const arg of Array.isArray(xpr.args) && xpr.args || []) { // TODO named args?
685
- if (isVirtualElement(arg._artifact || arg))
686
- error('ref-unexpected-virtual', arg.location, { '#': 'expr' });
687
-
688
- // Arg must not be an association and not $self
689
- // Only if path is not approved exists path (that is non-query position)
690
- if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
691
- if (arg.$expected === 'exists')
692
- error('ref-unexpected-assoc', arg.location, { '#': 'expr' } );
693
- }
694
- else if (!allowAssocTail && isAssociationOperand(arg)) {
695
- error('ref-unexpected-assoc', arg.location, { '#': 'expr' } );
627
+ // Only check associations and $self if this is not a backlink-like
628
+ // expression (a comparison of $self with an assoc).
629
+ // We don't check token-stream-like 'xpr's.
630
+ const isNotSelfComparison = xpr.op?.val !== 'xpr' &&
631
+ !isBinaryDollarSelfComparisonWithAssoc(xpr);
632
+ const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
633
+ const op = getBinaryOp(xpr);
634
+
635
+ if (isNotSelfComparison) {
636
+ for (const arg of args) {
637
+ if (op?.val !== '=' && isDollarSelfOrProjectionOperand(arg)) {
638
+ // `nary` operators don't have a "good" location; use $self in that case.
639
+ const loc = (op?.location.endLine ? op : arg).location;
640
+ error('expr-invalid-operator', [ loc, user ], { op: '=', id: '$self' });
641
+ }
642
+ else {
643
+ checkExpressionIsNotAssocOrSelf(arg, user, allowAssocTail);
644
+ }
696
645
  }
646
+ }
647
+ }
697
648
 
698
- if (isDollarSelfOrProjectionOperand(arg)) {
699
- error(null, arg.location, { id: arg.path[0].id },
700
- '$(ID) can only be used as a value in a comparison to an association');
701
- }
649
+ function checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail ) {
650
+ // Arg must not be an association and not $self
651
+ // Only if path is not approved exists path (that is non-query position)
652
+ if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
653
+ if (arg.$expected === 'exists')
654
+ error('ref-unexpected-assoc', [ arg.location, user ], { '#': 'expr' } );
655
+ }
656
+ else if (!allowAssocTail && isAssociationOperand(arg)) {
657
+ error('ref-unexpected-assoc', [ arg.location, user ], { '#': 'expr' } );
658
+ }
702
659
 
703
- // Recursively traverse the argument expression
704
- checkTreeLikeExpression(arg, allowAssocTail);
660
+ if (isDollarSelfOrProjectionOperand(arg)) {
661
+ error(null, [ arg.location, user ], { id: arg.path[0].id },
662
+ '$(ID) can only be used as a value in a comparison to an association');
705
663
  }
706
664
  }
665
+
707
666
  // Return true if 'arg' is an expression argument of type association or composition
708
667
  function isAssociationOperand( arg ) {
709
668
  if (!arg.path) {
@@ -715,9 +674,11 @@ function check( model ) { // = XSN
715
674
  (arg._artifact && arg._artifact._effectiveType && arg._artifact._effectiveType.target);
716
675
  }
717
676
 
718
- // Return true if 'arg' is an expression argument denoting "$self" || "$projection"
677
+ /**
678
+ * Return true if 'arg' is an expression argument denoting "$self" || "$projection"
679
+ */
719
680
  function isDollarSelfOrProjectionOperand( arg ) {
720
- return arg.path && arg.path.length === 1 &&
681
+ return arg.path?.length === 1 &&
721
682
  (arg.path[0].id === '$self' || arg.path[0].id === '$projection');
722
683
  }
723
684
 
@@ -739,6 +700,11 @@ function check( model ) { // = XSN
739
700
  return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[1]) ||
740
701
  isAssociationOperand(xpr.args[1]) && isDollarSelfOrProjectionOperand(xpr.args[0]));
741
702
  }
703
+ else if (xpr.args.length === 3 && xpr.args[1].val === '=') {
704
+ // Tree-ish expression from the compiler (not augmented)
705
+ return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[2]) ||
706
+ isAssociationOperand(xpr.args[2]) && isDollarSelfOrProjectionOperand(xpr.args[0]));
707
+ }
742
708
 
743
709
  // Nothing else qualifies
744
710
  return false;
@@ -811,12 +777,12 @@ function check( model ) { // = XSN
811
777
  // Must have literal or path unless it is a boolean
812
778
  if (!anno.literal && !anno.path && getFinalTypeNameOf(elementDecl) !== 'cds.Boolean') {
813
779
  if (elementDecl.type && elementDecl.type._artifact.name.absolute) {
814
- warning(null, anno.location || anno.name.location, { type: elementDecl.type._artifact },
815
- 'Expecting a value of type $(TYPE) for the annotation');
780
+ warning('anno-expecting-value', anno.location || anno.name.location,
781
+ { '#': 'type', type: elementDecl.type._artifact });
816
782
  }
817
783
  else {
818
- warning(null, anno.location || anno.name.location, {},
819
- 'Expecting a value for the annotation');
784
+ warning('anno-expecting-value', anno.location || anno.name.location,
785
+ { '#': 'std', anno: anno.name.absolute });
820
786
  }
821
787
 
822
788
  return;
@@ -950,12 +916,13 @@ function check( model ) { // = XSN
950
916
 
951
917
  // Continue search with next path step
952
918
  const nextStepEnv = (from._effectiveType || from).artifacts ||
953
- from._effectiveType.elements || [];
919
+ from._effectiveType?.elements || [];
954
920
  return resolvePathFrom(path.slice(1), nextStepEnv[path[0].id], result);
955
921
  }
956
922
 
957
- // Return the absolute name of the final type of 'node'. May return 'undefined' for
958
- // anonymous types
923
+ // Return the absolute name of the final type of 'node'. May return 'undefined'
924
+ // for anonymous types. DO NOT USE THIS function, it has several assumptions
925
+ // which are not necessarily true.
959
926
  function getFinalTypeNameOf( node ) {
960
927
  let type = node._effectiveType;
961
928
  if (type.type)
@@ -1009,21 +976,49 @@ function checkSapCommonLocale( model ) {
1009
976
  }
1010
977
  }
1011
978
 
1012
- // For each property named 'path' in 'node' (recursively), call callback(path, node)
1013
- //
1014
- // TODO: remove - this is not a good way to traverse expressions
1015
- function foreachPath( node, callback ) {
1016
- if (node === null || typeof node !== 'object') {
1017
- // Primitive node
1018
- return;
979
+
980
+ /**
981
+ * Visits each expression.
982
+ *
983
+ * TODO: Properly visit expressions; will be improved step by step;
984
+ * Currently only replaces old foreachPath().
985
+ *
986
+ * @param {any} xpr
987
+ * @param {XSN.Artifact} user
988
+ * @param {(xpr: any, user: any, parentExpr: any) => void} callback
989
+ */
990
+ function visitExpression( xpr, user, callback ) {
991
+ callback( xpr, user, null );
992
+ visitSubExpression( xpr, user, callback );
993
+ }
994
+
995
+ /**
996
+ * Visits each sub-expression.
997
+ *
998
+ * @param {any} xpr
999
+ * @param {XSN.Artifact} user
1000
+ * @param {(xpr: any, user: any, parentExpr: any) => void} callback
1001
+ */
1002
+ function visitSubExpression( xpr, user, callback ) {
1003
+ if (xpr.args) {
1004
+ const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
1005
+ // Check for illegal argument usage within the expression
1006
+ for (const arg of args) {
1007
+ callback( arg, user, xpr.args );
1008
+ // Recursively traverse the argument expression
1009
+ visitSubExpression(arg, user, callback);
1010
+ }
1019
1011
  }
1020
- for (const name in node) {
1021
- // If path found within a non-dictionary, call callback
1022
- if (name === 'path' && Object.getPrototypeOf(node))
1023
- callback(node.path, node);
1024
- // Descend recursively
1025
- foreachPath(node[name], callback);
1012
+
1013
+ if (xpr.path?.length) {
1014
+ for (const arg of xpr.path) {
1015
+ if (arg.where) {
1016
+ callback( arg.where, user, arg );
1017
+ visitSubExpression(arg.where, user, callback);
1018
+ }
1019
+ }
1026
1020
  }
1027
1021
  }
1028
1022
 
1023
+
1029
1024
  module.exports = check;