@sap/cds-compiler 6.1.0 → 6.3.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 (90) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/bin/cdsc.js +17 -6
  3. package/bin/cdsse.js +1 -1
  4. package/bin/cdsv2m.js +1 -1
  5. package/lib/api/main.js +29 -7
  6. package/lib/api/options.js +1 -1
  7. package/lib/base/builtins.js +9 -0
  8. package/lib/base/keywords.js +1 -1
  9. package/lib/base/message-registry.js +41 -10
  10. package/lib/base/messages.js +13 -6
  11. package/lib/base/model.js +1 -1
  12. package/lib/base/optionProcessorHelper.js +7 -2
  13. package/lib/checks/assocOutsideService.js +17 -30
  14. package/lib/checks/checkForTypes.js +0 -18
  15. package/lib/checks/checkPathsInStoredCalcElement.js +2 -1
  16. package/lib/checks/featureFlags.js +4 -1
  17. package/lib/checks/onConditions.js +2 -2
  18. package/lib/checks/queryNoDbArtifacts.js +16 -15
  19. package/lib/checks/types.js +1 -1
  20. package/lib/checks/utils.js +30 -6
  21. package/lib/checks/validator.js +4 -5
  22. package/lib/compiler/assert-consistency.js +3 -1
  23. package/lib/compiler/base.js +1 -1
  24. package/lib/compiler/builtins.js +1 -1
  25. package/lib/compiler/checks.js +85 -39
  26. package/lib/compiler/define.js +24 -5
  27. package/lib/compiler/extend.js +1 -1
  28. package/lib/compiler/finalize-parse-cdl.js +9 -1
  29. package/lib/compiler/generate.js +4 -4
  30. package/lib/compiler/index.js +88 -6
  31. package/lib/compiler/lsp-api.js +2 -0
  32. package/lib/compiler/populate.js +8 -8
  33. package/lib/compiler/propagator.js +1 -1
  34. package/lib/compiler/resolve.js +22 -21
  35. package/lib/compiler/shared.js +6 -6
  36. package/lib/compiler/tweak-assocs.js +53 -31
  37. package/lib/compiler/utils.js +9 -16
  38. package/lib/compiler/xpr-rewrite.js +2 -2
  39. package/lib/gen/BaseParser.js +35 -29
  40. package/lib/gen/CdlGrammar.checksum +1 -1
  41. package/lib/gen/CdlParser.js +1424 -1430
  42. package/lib/gen/Dictionary.json +1 -2
  43. package/lib/gen/cdlKeywords.json +26 -0
  44. package/lib/inspect/inspectPropagation.js +1 -1
  45. package/lib/json/from-csn.js +2 -2
  46. package/lib/json/to-csn.js +1 -1
  47. package/lib/language/multiLineStringParser.js +1 -1
  48. package/lib/model/cloneCsn.js +1 -0
  49. package/lib/model/csnRefs.js +9 -4
  50. package/lib/model/csnUtils.js +67 -2
  51. package/lib/optionProcessor.js +9 -9
  52. package/lib/parsers/AstBuildingParser.js +28 -26
  53. package/lib/parsers/identifiers.js +2 -30
  54. package/lib/render/toCdl.js +73 -13
  55. package/lib/render/toSql.js +127 -108
  56. package/lib/render/utils/common.js +4 -2
  57. package/lib/render/utils/sql.js +67 -0
  58. package/lib/transform/addTenantFields.js +4 -4
  59. package/lib/transform/db/assertUnique.js +2 -1
  60. package/lib/transform/db/associations.js +37 -1
  61. package/lib/transform/db/assocsToQueries/transformExists.js +21 -32
  62. package/lib/transform/db/assocsToQueries/utils.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/expansion.js +37 -36
  65. package/lib/transform/db/killAnnotations.js +1 -0
  66. package/lib/transform/db/processSqlServices.js +20 -2
  67. package/lib/transform/draft/db.js +20 -20
  68. package/lib/transform/draft/odata.js +38 -40
  69. package/lib/transform/effective/associations.js +1 -1
  70. package/lib/transform/effective/flattening.js +40 -47
  71. package/lib/transform/effective/main.js +6 -4
  72. package/lib/transform/forOdata.js +201 -92
  73. package/lib/transform/forRelationalDB.js +151 -142
  74. package/lib/transform/localized.js +116 -109
  75. package/lib/transform/odata/adaptAnnotationRefs.js +21 -16
  76. package/lib/transform/odata/createForeignKeys.js +73 -70
  77. package/lib/transform/odata/flattening.js +216 -200
  78. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +47 -45
  79. package/lib/transform/odata/toFinalBaseType.js +40 -39
  80. package/lib/transform/odata/typesExposure.js +151 -133
  81. package/lib/transform/odata/utils.js +7 -6
  82. package/lib/transform/parseExpr.js +165 -162
  83. package/lib/transform/transformUtils.js +184 -551
  84. package/lib/transform/translateAssocsToJoins.js +511 -596
  85. package/lib/transform/tupleExpansion.js +495 -0
  86. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  87. package/lib/utils/moduleResolve.js +1 -1
  88. package/package.json +2 -2
  89. package/lib/base/cleanSymbols.js +0 -17
  90. package/lib/checks/nonexpandableStructured.js +0 -39
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { isPersistedOnDatabase, hasPersistenceSkipAnnotation } = require('../model/csnUtils');
3
+ const { isPersistedOnDatabase, applyTransformationsOnNonDictionary } = require('../model/csnUtils');
4
4
  const { isBuiltinType } = require('../base/builtins');
5
5
  const { requireForeignKeyAccess } = require('./onConditions');
6
6
  const { pathId } = require('../model/csnRefs');
@@ -26,8 +26,13 @@ function checkQueryForNoDBArtifacts( query ) {
26
26
  for (const prop of generalQueryProperties) {
27
27
  const queryPart = (query.SELECT || query.SET)[prop];
28
28
  if (Array.isArray(queryPart)) {
29
- for (const part of queryPart)
30
- checkQueryRef.call(this, part, prop === 'columns');
29
+ const that = this;
30
+ applyTransformationsOnNonDictionary((query.SELECT || query.SET), prop, {
31
+ ref: (parent, _name, val, csnPath, tokenStream, refIndex) => {
32
+ const danglingAssocAllowed = tokenStream[refIndex - 1] !== 'exists';
33
+ checkQueryRef.call(that, parent, danglingAssocAllowed);
34
+ },
35
+ }, { skipStandard: { on: true } });
31
36
  }
32
37
  else if (typeof queryPart === 'object') {
33
38
  checkQueryRef.call(this, queryPart, prop === 'columns');
@@ -119,9 +124,10 @@ function _checkExpandInline( obj, previousRefs = [], previousLinks = [] ) {
119
124
  * @param {CSN.Path} ref
120
125
  * @param {object[]} _links
121
126
  * @param {CSN.Path} $path
122
- * @param {boolean} inColumns
127
+ * @param {boolean} danglingAssocAllowed usually optimised to foreign key hence allowed even if target is skipped,
128
+ * except in from or after exists
123
129
  */
124
- function _checkRef( ref, _links, $path, inColumns ) {
130
+ function _checkRef( ref, _links, $path, danglingAssocAllowed ) {
125
131
  if (!ref || !_links )
126
132
  return;
127
133
 
@@ -129,7 +135,7 @@ function _checkRef( ref, _links, $path, inColumns ) {
129
135
  const isPublishedAssoc = this.csnUtils.isAssocOrComposition(_links[_links.length - 1].art);
130
136
 
131
137
  // Don't check the last element - to allow association publishing in columns
132
- for (let i = 0; i < (inColumns ? _links.length - 1 : _links.length); i++) {
138
+ for (let i = 0; i < (danglingAssocAllowed ? _links.length - 1 : _links.length); i++) {
133
139
  const link = _links[i];
134
140
  if (!link)
135
141
  continue;
@@ -153,8 +159,8 @@ function _checkRef( ref, _links, $path, inColumns ) {
153
159
  if (nonPersistedTarget) {
154
160
  let isJoinRelevant = isPublishedAssoc || // publishing associations is always join relevant
155
161
  isLast || // e.g. FROM targets are always join relevant.
156
- isUnmanagedOrNoKeys; // unmanaged associations are always join relevant -> no FKs
157
-
162
+ isUnmanagedOrNoKeys || // unmanaged associations are always join relevant -> no FKs
163
+ ref.slice(i).some(s => s.where || s.args); // function calls or filters are always join relevant
158
164
  if (!isJoinRelevant) {
159
165
  // for managed, published associations with more than one $path-step, only FK
160
166
  // access is allowed.
@@ -164,17 +170,12 @@ function _checkRef( ref, _links, $path, inColumns ) {
164
170
  }
165
171
 
166
172
  if (isJoinRelevant) {
167
- const cdsPersistenceSkipped = hasPersistenceSkipAnnotation(targetArt);
168
- this.error( null, $path, {
169
- '#': cdsPersistenceSkipped ? 'std' : 'abstract',
173
+ this.error('ref-invalid-assoc-navigation', $path, {
170
174
  anno: '@cds.persistence.skip',
171
175
  id: nonPersistedTarget.pathStep,
172
176
  elemref: { ref },
173
177
  name: nonPersistedTarget.name,
174
- }, {
175
- std: 'Unexpected $(ANNO) annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
176
- abstract: 'Unexpected abstract association target $(NAME) of $(ID) in path $(ELEMREF)',
177
- } );
178
+ });
178
179
  break; // only one error per path
179
180
  }
180
181
  }
@@ -152,7 +152,7 @@ function checkTypeOfHasProperType( artOrElement, name, model, error, path, deriv
152
152
 
153
153
 
154
154
  /**
155
- * Can happen in CSN, e.g. `{ a: { kind: "type" } }` but should not happen in CDL.
155
+ * Can happen in CSN, e.g. `{ a: { kind: "type" } }` or via `elem;` in CDL.
156
156
  *
157
157
  * @param {Function} error the error function
158
158
  * @param {CSN.Path} path the path to the element or the artifact
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { isBuiltinType } = require('../base/builtins');
4
- const { RelationalOperators } = require('../transform/transformUtils');
4
+ const { RelationalOperators } = require('../transform/tupleExpansion');
5
5
  /**
6
6
  * Prepare the ref steps so that they are loggable
7
7
  *
@@ -17,7 +17,9 @@ function logReady( refStep ) {
17
17
  * structured that can be used for tuple expansion. This can either be a
18
18
  * real 'elements' thing or a managed association/composition with foreign keys.
19
19
  *
20
- * The RHS may be 'null' or any value
20
+ * @TODO: This function also allows `is null` on the right-hand-side.
21
+ * We should move these checks to the actual tuple expansion, because
22
+ * if we're missing cases here, it currently results in incorrect expansion.
21
23
  *
22
24
  * @param {Array} on the on condition which to check
23
25
  * @param {number} startIndex the index of the relational term in the on condition array
@@ -26,8 +28,7 @@ function logReady( refStep ) {
26
28
  function otherSideIsExpandableStructure( on, startIndex ) {
27
29
  if (on[startIndex - 1] && RelationalOperators.includes(on[startIndex - 1])) {
28
30
  const lhs = on[startIndex - 2];
29
- // if ever lhs is allowed to be a value uncomment this
30
- return /* lhs?.val !== undefined || */ isOk(resolveArtifactType.call(this, lhs?._art));
31
+ return isOk(resolveArtifactType.call(this, lhs?._art));
31
32
  }
32
33
  else if (on[startIndex + 1] && RelationalOperators.includes(on[startIndex + 1])) {
33
34
  const op = on[startIndex + 1];
@@ -35,8 +36,7 @@ function otherSideIsExpandableStructure( on, startIndex ) {
35
36
  if (op === 'is')
36
37
  // check for unary operator 'is [not] null' as token stream
37
38
  return rhs === 'null' || (rhs === 'not' && on[startIndex + 3] === 'null');
38
- // if ever rhs is allowed to be a value uncomment this
39
- return /* rhs?.val !== undefined || */ isOk(resolveArtifactType.call(this, rhs?._art));
39
+ return isOk(resolveArtifactType.call(this, rhs?._art));
40
40
  }
41
41
  return false;
42
42
 
@@ -51,6 +51,29 @@ function otherSideIsExpandableStructure( on, startIndex ) {
51
51
  }
52
52
  }
53
53
 
54
+ /**
55
+ * Check that the opposite operand to a relational term is s value or "is null".
56
+ *
57
+ * @param {Array} expr the expression which to check
58
+ * @param {number} startIndex the index of the relational term in the expression array
59
+ * @returns {boolean} indicates whether the other side of a relational term is scalar
60
+ */
61
+ function otherSideIsValue( expr, startIndex ) {
62
+ if (expr[startIndex - 1] && RelationalOperators.includes(expr[startIndex - 1]))
63
+ return expr[startIndex - 2]?.val !== undefined;
64
+
65
+ if (expr[startIndex + 1] && RelationalOperators.includes(expr[startIndex + 1])) {
66
+ const op = expr[startIndex + 1];
67
+ const rhs = expr[startIndex + 2];
68
+ if (op === 'is')
69
+ // check for unary operator 'is [not] null' as token stream
70
+ return rhs === 'null' || (rhs === 'not' && expr[startIndex + 3] === 'null');
71
+ return rhs?.val !== undefined;
72
+ }
73
+
74
+ return false;
75
+ }
76
+
54
77
  /**
55
78
  * Get the real type of an artifact
56
79
  *
@@ -68,5 +91,6 @@ function resolveArtifactType( art ) {
68
91
  module.exports = {
69
92
  logReady,
70
93
  otherSideIsExpandableStructure,
94
+ otherSideIsValue,
71
95
  resolveArtifactType,
72
96
  };
@@ -42,7 +42,6 @@ const checkForInvalidTarget = require('./invalidTarget');
42
42
  const { validateAssociationsInItems } = require('./arrayOfs');
43
43
  const checkQueryForNoDBArtifacts = require('./queryNoDbArtifacts');
44
44
  const checkExplicitlyNullableKeys = require('./nullableKeys');
45
- const nonexpandableStructuredInExpression = require('./nonexpandableStructured');
46
45
  const existsMustEndInAssoc = require('./existsMustEndInAssoc');
47
46
  const forbidAssocInExists = require('./existsExpressionsOnlyForeignKeys');
48
47
  const checkPathsInStoredCalcElement = require('./checkPathsInStoredCalcElement');
@@ -87,7 +86,6 @@ const forRelationalDBCsnValidators = [
87
86
  checkCdsMap,
88
87
  existsMustEndInAssoc,
89
88
  forbidAssocInExists,
90
- nonexpandableStructuredInExpression,
91
89
  navigationIntoMany,
92
90
  checkPathsInStoredCalcElement,
93
91
  featureFlags,
@@ -124,7 +122,7 @@ const forOdataArtifactValidators
124
122
  checkReadOnlyAndInsertOnly,
125
123
  ];
126
124
 
127
- const forOdataCsnValidators = [ checkCdsMap, nonexpandableStructuredInExpression ];
125
+ const forOdataCsnValidators = [ checkCdsMap ];
128
126
 
129
127
  const forOdataQueryValidators = [];
130
128
 
@@ -202,8 +200,6 @@ function getDBCsnValidators( options ) {
202
200
  validations.push(checkForParams.csnValidator);
203
201
  if (options.sqlDialect === 'h2' || options.sqlDialect === 'postgres')
204
202
  validations.push(checkForHanaTypes);
205
- if (options.transformation === 'effective' && options.effectiveServiceName)
206
- validations.push(assertNoAssocUsageOutsideOfService);
207
203
 
208
204
  return validations;
209
205
  }
@@ -232,6 +228,9 @@ function forRelationalDB( csn, that ) {
232
228
  },
233
229
  (artifact, artifactName) => {
234
230
  if (that.options.transformation === 'effective') {
231
+ if (that.options.effectiveServiceName)
232
+ assertNoAssocUsageOutsideOfService.bind(that)(artifact, artifactName);
233
+
235
234
  forEachMemberRecursively(artifact, checkAnnotationExpression.bind(that), [ 'definitions', artifactName ], false, {
236
235
  skipArtifact: a => a.returns || (a.params && !a.query),
237
236
  });
@@ -647,6 +647,8 @@ function assertConsistency( model, stage ) {
647
647
  'where', 'columns', 'mixin', 'quantifier', 'offset',
648
648
  'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having',
649
649
  '$limit', 'limit', '_status', '_origin',
650
+ // via casts
651
+ 'enum',
650
652
  ],
651
653
  },
652
654
  _leadingQuery: { kind: true, test: TODO },
@@ -1059,7 +1061,7 @@ function assertConsistency( model, stage ) {
1059
1061
  // TODO
1060
1062
  // else if (spec.instanceOf && spec.instanceOf !== 'ignore' &&
1061
1063
  // Object.getPrototypeOf( node ) !== spec.instanceOf.prototype)
1062
- // eslint-disable-next-line @stylistic/js/max-len
1064
+ // eslint-disable-next-line @stylistic/max-len
1063
1065
  // throw new InternalConsistencyError( `Expected object of class ${ spec.instanceOf.name } but found ${ found }${ at( [ null, parent ], prop, name ) }` );
1064
1066
  }
1065
1067
 
@@ -21,7 +21,7 @@ const kindProperties = {
21
21
  entity: {
22
22
  elements: true, actions: true, params: () => false, include: true,
23
23
  },
24
- select: { normalized: 'select', elements: true },
24
+ select: { normalized: 'select', elements: propExists, enum: propExists },
25
25
  $join: { normalized: 'select' },
26
26
  $tableAlias: { normalized: 'alias' }, // table alias in select
27
27
  $self: { normalized: 'alias' }, // table alias in select
@@ -222,7 +222,7 @@ const dateRegEx = /^(-?\d{4})-(\d{1,2})-(\d{1,2})$/;
222
222
  // YYYY - MM - dd
223
223
  const timeRegEx = /^T?(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:Z|[+-]\d{2}(?::\d{2})?)?$/;
224
224
  // T HH : mm : ss TZD
225
- // eslint-disable-next-line @stylistic/js/max-len, sonarjs/regex-complexity
225
+ // eslint-disable-next-line @stylistic/max-len, sonarjs/regex-complexity
226
226
  const timestampRegEx = /^(-?\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,7})?)?(?:Z|[+-]\d{2}(?::\d{2})?)?$/;
227
227
  // YYYY - MM - dd T HH : mm : ss . fraction TZD
228
228
  // eslint-disable-next-line sonarjs/regex-complexity
@@ -5,7 +5,6 @@
5
5
  // * Different ad-hoc value/type checks (associations, enum, ...) -
6
6
  // specify a proper one and use consistently
7
7
  // * Using name comparisons instead proper object comparisons.
8
- // * effectiveType issues.
9
8
  // * Often forgot to consider CSN input
10
9
 
11
10
  'use strict';
@@ -18,7 +17,7 @@ const {
18
17
  isDeprecatedEnabled,
19
18
  } = require('../base/model');
20
19
  const { typeParameters } = require('./builtins');
21
- const { propagationRules } = require('../base/builtins');
20
+ const { propagationRules, acceptsExprValues } = require('../base/builtins');
22
21
  const { annotationVal } = require('./utils');
23
22
 
24
23
  const $location = Symbol.for( 'cds.$location' );
@@ -84,7 +83,7 @@ function check( model ) {
84
83
  checkName( art );
85
84
  checkTypeArguments( art );
86
85
 
87
- if (art.value && !art.$calcDepElement && art.type)
86
+ if (art.value && !art.$calcDepElement && (art.type || art.elements || art.items))
88
87
  checkTypeCast( art.value, art );
89
88
 
90
89
  for (const anno of iterateAnnotations( art ))
@@ -117,13 +116,21 @@ function check( model ) {
117
116
  return;
118
117
 
119
118
  const isVirtual = parentProps.virtual?.val || elem.virtual?.val;
119
+ const typeName = elem._effectiveType?.name?.id;
120
120
  if (isVirtual) {
121
121
  error( 'def-unexpected-key', [ (parentProps.key || elem.key).location, elem ],
122
- { '#': 'virtual', prop: 'key' } );
122
+ { '#': 'virtual', keyword: 'key' } );
123
123
  }
124
- else if (elem._effectiveType?.name?.id === 'cds.Map') {
124
+ else if (typeName === 'cds.Map') {
125
125
  error( 'def-unexpected-key', [ elem.type?.location || elem.location, elem ],
126
- { '#': 'invalidType', prop: 'key', type: 'cds.Map' } );
126
+ { '#': 'invalidType', keyword: 'key', type: typeName } );
127
+ }
128
+ else if (typeName === 'cds.LargeString' ||
129
+ typeName === 'cds.Vector' ||
130
+ typeName === 'cds.hana.CLOB' ||
131
+ typeName === 'cds.LargeBinary') {
132
+ warning( 'def-unsupported-key', [ elem.type?.location || elem.location, elem ],
133
+ { '#': 'type', keyword: 'key', type: typeName } );
127
134
  }
128
135
  }
129
136
 
@@ -245,25 +252,43 @@ function check( model ) {
245
252
  }
246
253
 
247
254
  function checkTypeCast( xpr, user ) {
248
- const isCast = (xpr.op?.val === 'cast');
249
- const elem = isCast
255
+ const isSqlCast = (xpr.op?.val === 'cast');
256
+ const elem = isSqlCast
250
257
  ? xpr.args?.[0]?._artifact
251
258
  : xpr._artifact;
252
- const type = isCast ? xpr.type : user.type;
253
- if (!isCast && type.$inferred)
259
+
260
+ const typeArt = isSqlCast ? xpr : user;
261
+ if (!elem || !isSqlCast && typeArt.type?.$inferred)
254
262
  return; // e.g. $inferred:'generated'
255
- if (elem && type) { // has explicit type
256
- if (type._artifact?._effectiveType?.name.id === 'cds.Map')
263
+
264
+ const { type } = typeArt;
265
+ if (type) { // has explicit type
266
+ if (type._artifact?._effectiveType?.name.id === 'cds.Map') {
257
267
  error( 'type-invalid-cast', [ type.location, user ], { '#': 'std', type: 'cds.Map' } );
258
- else if (type._artifact?.elements)
268
+ }
269
+ else if (type._artifact?.elements) {
259
270
  error( 'type-invalid-cast', [ type.location, user ], { '#': 'to-structure' } );
260
- else if (elem.elements) // TODO: calc elements
271
+ }
272
+ else if (elem.elements) { // TODO: calc elements
261
273
  error( 'type-invalid-cast', [ type.location, user ], { '#': 'from-structure' } );
262
- else if (elem.target && !type._artifact?.target)
274
+ }
275
+ else if (elem.target && !type._artifact?.target) {
263
276
  error( 'type-invalid-cast', [ type.location, user ], { '#': 'from-assoc' } );
264
- else if (!elem.target && type._artifact?.target && !user.type?.$inferred)
265
- // $inferred already reported in resolve.js
266
- error( 'type-invalid-cast', [ type.location, user ], { '#': 'assoc' } );
277
+ }
278
+ else if (!elem.target && // referenced element is not association
279
+ !user.type?.$inferred && // $inferred types already reported in resolve.js.
280
+ (
281
+ // assoc used in SQL cast
282
+ type._artifact?.target && isSqlCast ||
283
+ // there is a target and the type is a direct `cds.Association`;
284
+ // other types handled by resolver already.
285
+ typeArt.target && type._artifact?.category === 'relation'
286
+ )
287
+ ) {
288
+ // - redirection-check in resolve.js already checks this for CDL-casts
289
+ // - `"cast": { "target": "…", "type": "cds.Association", … }` via CSN input.
290
+ error('type-invalid-cast', [ type.location, user ], { '#': 'assoc' });
291
+ }
267
292
  }
268
293
  }
269
294
 
@@ -687,12 +712,15 @@ function check( model ) {
687
712
  *
688
713
  * @param {any} xpr The expression to check
689
714
  * @param {XSN.Artifact} user User for semantic location
715
+ * @param {any} _parentExpr
690
716
  * @param {string} [context] where the expression is used, e.g. 'anno'
691
717
  */
692
- function checkGenericExpression( xpr, user, context ) {
718
+ function checkGenericExpression( xpr, user, _parentExpr, context ) {
693
719
  if (context !== 'anno')
694
720
  checkExpressionNotVirtual( xpr, user );
695
- checkExpressionAssociationUsage( xpr, user, false );
721
+ checkExpressionAssociationUsage( xpr, user, {
722
+ context, rejectManaged: context === 'anno', rejectUnmanaged: true,
723
+ } );
696
724
  if (xpr.op?.val === 'cast') {
697
725
  requireExplicitTypeInSqlCast( xpr, user );
698
726
  checkTypeCast( xpr, user );
@@ -713,7 +741,7 @@ function check( model ) {
713
741
 
714
742
  visitExpression( elem.on, elem, (xpr, user) => {
715
743
  checkExpressionNotVirtual( xpr, user );
716
- checkExpressionAssociationUsage( xpr, user, true );
744
+ checkExpressionAssociationUsage( xpr, user, null );
717
745
 
718
746
  if (xpr._artifact?._effectiveType?.name.id === 'cds.Map') {
719
747
  error( 'ref-unexpected-map', [ xpr.location, user ], { '#': 'onCond', type: 'cds.Map' } );
@@ -727,7 +755,9 @@ function check( model ) {
727
755
  }
728
756
 
729
757
  function checkSelectItemValue( elem ) {
730
- checkExpressionAssociationUsage( elem.value, elem, false );
758
+ checkExpressionAssociationUsage( elem.value, elem, {
759
+ context: 'query', rejectManaged: false, rejectUnmanaged: true,
760
+ } );
731
761
  checkVirtualSelectItemChangeForV6( elem );
732
762
  // To avoid duplicate messages, only run this check if the type wasn't inferred from
733
763
  // the cast, as otherwise we will check it twice (once here, once via element).
@@ -736,8 +766,8 @@ function check( model ) {
736
766
  checkTypeCast( elem.value, elem );
737
767
  checkTypeArguments( elem.value, elem );
738
768
  }
739
- visitSubExpression( elem.value, elem, (xpr) => {
740
- checkGenericExpression( xpr, elem );
769
+ visitSubExpression( elem.value, elem, (xpr, user, parentExpr) => {
770
+ checkGenericExpression( xpr, elem, parentExpr, 'query' );
741
771
  } );
742
772
  }
743
773
 
@@ -794,7 +824,8 @@ function check( model ) {
794
824
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
795
825
  // And users can't change structured to non-structured elements.
796
826
  if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
797
- error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
827
+ error( 'ref-unexpected-structured', [ sourceLoc, elem ],
828
+ { '#': 'struct-expr', elemref: xpr } );
798
829
  }
799
830
  else if (xpr._artifact.target !== undefined && (!lastStep.where || isStored)) {
800
831
  // Allow using an association _with filter_, but only for on-read calculated elements.
@@ -867,10 +898,11 @@ function check( model ) {
867
898
  *
868
899
  * @param {any} xpr The expression to check
869
900
  * @param {XSN.Artifact} user
870
- * @param {boolean} allowAssocTail
901
+ * @param {{context: string, rejectUnmanaged, rejectManaged}|null} [rejectAssocTail]
902
+ * Context where association tails are not allowed.
871
903
  * @returns {void}
872
904
  */
873
- function checkExpressionAssociationUsage( xpr, user, allowAssocTail ) {
905
+ function checkExpressionAssociationUsage( xpr, user, rejectAssocTail = null ) {
874
906
  if (!xpr.args)
875
907
  return;
876
908
 
@@ -885,12 +917,18 @@ function check( model ) {
885
917
  const op = getBinaryOp( xpr );
886
918
  for (const arg of args) {
887
919
  if (arg && !(op?.val !== '=' && isDollarSelfOrProjectionOperand( arg )))
888
- checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail );
920
+ checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail );
889
921
  }
890
922
  }
891
923
  }
892
924
 
893
- function checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail ) {
925
+ /**
926
+ * @param arg
927
+ * @param {XSN.Artifact} user
928
+ * @param {{context: string, rejectUnmanaged, rejectManaged}|null} [rejectAssocTail]
929
+ * Context where association tails are not allowed.
930
+ */
931
+ function checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail ) {
894
932
  // Arg must not be an association and not $self
895
933
  // Only if path is not approved exists path (that is non-query position)
896
934
  if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
@@ -899,9 +937,17 @@ function check( model ) {
899
937
  error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
900
938
  }
901
939
  }
902
- else if (!allowAssocTail && isAssociationOperand( arg )) {
903
- const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
904
- error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
940
+ else if (rejectAssocTail && isAssociationOperand( arg )) {
941
+ if (rejectAssocTail.rejectManaged && rejectAssocTail.rejectUnmanaged ||
942
+ rejectAssocTail.rejectManaged && arg._artifact.keys ||
943
+ rejectAssocTail.rejectUnmanaged && arg._artifact.on) {
944
+ // only a few contexts have special message
945
+ const context = rejectAssocTail.context === 'query' && 'query-' ||
946
+ rejectAssocTail.context === 'anno' && 'anno-' ||
947
+ '';
948
+ const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
949
+ error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': `${ context }${ variant }` } );
950
+ }
905
951
  }
906
952
  }
907
953
 
@@ -935,16 +981,16 @@ function check( model ) {
935
981
  // One argument must be "$self" and the other an assoc
936
982
  if (xpr.op.val === '=' && xpr.args.length === 2) {
937
983
  // Tree-ish expression from the compiler (not augmented)
938
- // eslint-disable-next-line @stylistic/js/max-len
984
+ // eslint-disable-next-line @stylistic/max-len
939
985
  return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[1] ) ||
940
- // eslint-disable-next-line @stylistic/js/max-len
986
+ // eslint-disable-next-line @stylistic/max-len
941
987
  isAssociationOperand( xpr.args[1] ) && isDollarSelfOrProjectionOperand( xpr.args[0] ));
942
988
  }
943
989
  else if (xpr.args.length === 3 && xpr.args[1].val === '=') {
944
990
  // Tree-ish expression from the compiler (not augmented)
945
- // eslint-disable-next-line @stylistic/js/max-len
991
+ // eslint-disable-next-line @stylistic/max-len
946
992
  return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[2] ) ||
947
- // eslint-disable-next-line @stylistic/js/max-len
993
+ // eslint-disable-next-line @stylistic/max-len
948
994
  isAssociationOperand( xpr.args[2] ) && isDollarSelfOrProjectionOperand( xpr.args[0] ));
949
995
  }
950
996
 
@@ -963,7 +1009,7 @@ function check( model ) {
963
1009
  const name = anno.name?.id;
964
1010
  if (!name)
965
1011
  return true;
966
- if (!propagationRules[`@${ name }`])
1012
+ if (!propagationRules[`@${ name }`] || acceptsExprValues[`@${ name }`])
967
1013
  return true;
968
1014
  error( 'anno-unexpected-expr', [ anno.location, art ], { anno: name },
969
1015
  'Unexpected expression as value for $(ANNO)' );
@@ -1088,7 +1134,7 @@ function check( model ) {
1088
1134
  */
1089
1135
  function checkAnnotationExpressions( anno, art ) {
1090
1136
  if (anno.$tokenTexts) {
1091
- checkGenericExpression( anno, art, 'anno' );
1137
+ checkGenericExpression( anno, art, null, 'anno' );
1092
1138
  }
1093
1139
  else if (anno.literal === 'array') {
1094
1140
  anno.val.forEach( val => checkAnnotationExpressions( val, art ) );
@@ -1165,7 +1211,7 @@ function check( model ) {
1165
1211
  value.literal !== 'timestamp' && value.literal !== 'string') {
1166
1212
  // Hm, actually date and time cannot be mixed
1167
1213
  warning( null, loc, { type, anno },
1168
- // eslint-disable-next-line @stylistic/js/max-len
1214
+ // eslint-disable-next-line @stylistic/max-len
1169
1215
  'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)' );
1170
1216
  }
1171
1217
  }
@@ -1010,6 +1010,7 @@ function define( model ) {
1010
1010
  }
1011
1011
  // Either expression (value), expand, new virtual or new association
1012
1012
  else if (col.value || col.name) {
1013
+ col.kind = 'element';
1013
1014
  if (!col._block)
1014
1015
  setLink( col, '_block', parent._block );
1015
1016
  if (col.inline) { // `@anno elem.{ * }` does not work
@@ -1031,8 +1032,7 @@ function define( model ) {
1031
1032
  initSelectItems( col, null, user ); // TODO: use col as user (i.e. remove param)
1032
1033
  }
1033
1034
 
1034
- initItemsLinks( col, parent._block );
1035
- initExprAnnoBlock( col, parent._block );
1035
+ initCdlTypeCast( col, parent );
1036
1036
  }
1037
1037
 
1038
1038
  if (hasItems && !wildcard && parent.excludingDict && !options.$recompile) {
@@ -1044,6 +1044,26 @@ function define( model ) {
1044
1044
  }
1045
1045
  }
1046
1046
 
1047
+ function initCdlTypeCast( col, parent ) {
1048
+ if (col.val)
1049
+ return; // e.g. '*' column
1050
+
1051
+ setMemberParent( col, col.name, parent );
1052
+ initMembers( col, col, col._block );
1053
+
1054
+ // We don't allow CDL-style casts to anonymous structures. We reject it already here
1055
+ // and not in checks.js to ensure that it's rejected in parseCdl.
1056
+ if (col.elements) {
1057
+ error('type-invalid-cast', [ col.elements[$location] ?? col.location, col ],
1058
+ { '#': 'to-inline-structure' });
1059
+ }
1060
+ else if (col.expand && (col.type || col.elements || col.items)) {
1061
+ const loc = (col.type?.location || col.elements?.[$location] ||
1062
+ col.items?.location || col.location);
1063
+ error('type-invalid-cast', [ loc, col ], { '#': 'expand' });
1064
+ }
1065
+ }
1066
+
1047
1067
  /**
1048
1068
  * If we have a valid top-level exists, exists in filters of sub-expressions can be translated,
1049
1069
  * since we will have a top-level subquery after exists-processing in the forRelationalDB.
@@ -1150,7 +1170,7 @@ function define( model ) {
1150
1170
  // We do not want to complain separately about all element properties:
1151
1171
  error( 'ext-unexpected-element', [ e.location, construct ],
1152
1172
  { name: e.name.id, code: 'extend … with enum' },
1153
- // eslint-disable-next-line @stylistic/js/max-len
1173
+ // eslint-disable-next-line @stylistic/max-len
1154
1174
  'Unexpected elements like $(NAME) in an extension for an enum. Additionally, use $(CODE) when extending enums' );
1155
1175
  // Don't emit 'ext-expecting-enum' if this error is emitted.
1156
1176
  return;
@@ -1238,7 +1258,6 @@ function define( model ) {
1238
1258
  if (elem.$duplicates === true && add)
1239
1259
  elem.$duplicates = null;
1240
1260
  setMemberParent( elem, name, parent, add && prop );
1241
- // console.log(message( null, elem.location, elem, {}, 'Info', 'INIT').toString())
1242
1261
  checkRedefinition( elem );
1243
1262
  initMembers( elem, elem, bl, initExtensions );
1244
1263
  if (boundSelfParamType && (elem.kind === 'action' || elem.kind === 'function'))
@@ -1320,7 +1339,7 @@ function define( model ) {
1320
1339
  // - artifacts (CDL-only anyway) only inside [extend] context|service
1321
1340
  if (!dict)
1322
1341
  return false;
1323
- const feature = kindProperties[parent.kind][prop];
1342
+ const feature = kindProperties[parent.kind ?? 'element'][prop];
1324
1343
  if (feature &&
1325
1344
  (feature === true || construct.kind !== 'extend' || feature( prop, parent )))
1326
1345
  return true;
@@ -561,7 +561,7 @@ function extend( model ) {
561
561
  {
562
562
  std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)',
563
563
  array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)',
564
- // eslint-disable-next-line @stylistic/js/max-len
564
+ // eslint-disable-next-line @stylistic/max-len
565
565
  struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)',
566
566
  boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)',
567
567
  null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)',
@@ -117,7 +117,15 @@ function finalizeParseCdl( model ) {
117
117
  // containing it. Otherwise some type's aren't properly resolved.
118
118
  // TODO: If resolveTypeUnchecked is reworked, we may be able to simplify this coding.
119
119
  (artifact.$queries || []).forEach( art => resolveTypesForParseCdl( art, artifact ) );
120
- (artifact.columns || []).forEach( art => resolveTypesForParseCdl( art, artifact ) );
120
+ (artifact.columns || []).forEach( (col) => {
121
+ // TODO: Can we use "ensureColumnName" of populate.js? It depends on column indices
122
+ // _after_ wildcards were expanded, though.
123
+ if (!col.name && col.value?.path) {
124
+ const last = col.value.path.at(-1);
125
+ col.name = { id: last?.id || '', location: last?.location, $inferred: 'as' };
126
+ }
127
+ resolveTypesForParseCdl( col, artifact );
128
+ } );
121
129
  forEachGeneric( artifact, 'mixin', art => resolveTypesForParseCdl( art, artifact ) );
122
130
 
123
131
  // For better error messages for `type of`s in `returns`, we pass the object as the new main.
@@ -104,7 +104,7 @@ function generate( model ) {
104
104
  const lang = textsAspect.elements.language;
105
105
  error( 'def-unexpected-element', [ lang.name.location, lang ],
106
106
  { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
107
- // eslint-disable-next-line @stylistic/js/max-len
107
+ // eslint-disable-next-line @stylistic/max-len
108
108
  '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
109
109
  hasError = true;
110
110
  }
@@ -209,7 +209,7 @@ function generate( model ) {
209
209
  conflictingElements.push( elem );
210
210
 
211
211
  const isKey = elem.key && elem.key.val;
212
- const isLocalized = hasTruthyProp( elem, 'localized' );
212
+ const isLocalized = elem.$syntax !== 'calc' && hasTruthyProp( elem, 'localized' );
213
213
 
214
214
  if (isKey) {
215
215
  keys += 1;
@@ -245,7 +245,7 @@ function generate( model ) {
245
245
  (fioriEnabled && art.elements.ID_texts)) {
246
246
  // TODO if we have too much time: check all elements of texts entity for safety
247
247
  warning( null, [ art.name.location, art ], { art: textsEntity },
248
- // eslint-disable-next-line @stylistic/js/max-len
248
+ // eslint-disable-next-line @stylistic/max-len
249
249
  'Texts entity $(ART) can\'t be created as there is another definition with that name' );
250
250
  info( null, [ textsEntity.name.location, textsEntity ], { art },
251
251
  'Texts entity for $(ART) can\'t be created with this definition' );
@@ -640,7 +640,7 @@ function generate( model ) {
640
640
  }
641
641
  if (model.definitions[entityName]) {
642
642
  error( null, [ location, elem ], { art: entityName },
643
- // eslint-disable-next-line @stylistic/js/max-len
643
+ // eslint-disable-next-line @stylistic/max-len
644
644
  'Target entity $(ART) can\'t be created as there is another definition with this name' );
645
645
  return false;
646
646
  }