@sap/cds-compiler 3.7.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 (70) hide show
  1. package/CHANGELOG.md +63 -4
  2. package/bin/cdsc.js +3 -0
  3. package/doc/CHANGELOG_ARCHIVE.md +6 -6
  4. package/doc/CHANGELOG_BETA.md +15 -0
  5. package/doc/DeprecatedOptions_v2.md +1 -1
  6. package/doc/NameResolution.md +1 -1
  7. package/lib/api/main.js +61 -22
  8. package/lib/api/options.js +1 -0
  9. package/lib/api/validate.js +5 -0
  10. package/lib/base/dictionaries.js +5 -3
  11. package/lib/base/keywords.js +2 -0
  12. package/lib/base/message-registry.js +64 -22
  13. package/lib/base/messages.js +12 -7
  14. package/lib/base/model.js +3 -2
  15. package/lib/checks/arrayOfs.js +1 -1
  16. package/lib/checks/defaultValues.js +1 -1
  17. package/lib/checks/hasPersistedElements.js +1 -1
  18. package/lib/checks/invalidTarget.js +1 -1
  19. package/lib/checks/onConditions.js +9 -6
  20. package/lib/checks/sql-snippets.js +2 -2
  21. package/lib/checks/types.js +1 -2
  22. package/lib/compiler/assert-consistency.js +24 -5
  23. package/lib/compiler/base.js +49 -2
  24. package/lib/compiler/builtins.js +15 -6
  25. package/lib/compiler/checks.js +4 -4
  26. package/lib/compiler/define.js +59 -80
  27. package/lib/compiler/extend.js +701 -498
  28. package/lib/compiler/finalize-parse-cdl.js +4 -3
  29. package/lib/compiler/index.js +1 -1
  30. package/lib/compiler/kick-start.js +2 -2
  31. package/lib/compiler/populate.js +17 -9
  32. package/lib/compiler/propagator.js +12 -5
  33. package/lib/compiler/resolve.js +26 -173
  34. package/lib/compiler/shared.js +12 -53
  35. package/lib/compiler/tweak-assocs.js +1 -1
  36. package/lib/compiler/utils.js +2 -2
  37. package/lib/edm/annotations/genericTranslation.js +124 -46
  38. package/lib/edm/csn2edm.js +22 -1
  39. package/lib/edm/edmPreprocessor.js +41 -21
  40. package/lib/gen/Dictionary.json +4 -0
  41. package/lib/gen/language.checksum +1 -1
  42. package/lib/gen/language.interp +3 -1
  43. package/lib/gen/languageLexer.js +1 -1
  44. package/lib/gen/languageParser.js +4810 -4482
  45. package/lib/inspect/inspectPropagation.js +20 -36
  46. package/lib/json/from-csn.js +55 -5
  47. package/lib/json/to-csn.js +71 -110
  48. package/lib/language/errorStrategy.js +1 -0
  49. package/lib/language/genericAntlrParser.js +47 -8
  50. package/lib/language/language.g4 +88 -62
  51. package/lib/language/textUtils.js +13 -0
  52. package/lib/main.d.ts +43 -3
  53. package/lib/main.js +4 -2
  54. package/lib/model/csnRefs.js +14 -2
  55. package/lib/model/csnUtils.js +11 -74
  56. package/lib/model/revealInternalProperties.js +3 -0
  57. package/lib/optionProcessor.js +3 -0
  58. package/lib/render/toCdl.js +203 -104
  59. package/lib/render/toHdbcds.js +0 -1
  60. package/lib/render/toRename.js +14 -51
  61. package/lib/transform/braceExpression.js +6 -0
  62. package/lib/transform/db/rewriteCalculatedElements.js +55 -14
  63. package/lib/transform/forOdataNew.js +20 -15
  64. package/lib/transform/forRelationalDB.js +21 -14
  65. package/lib/transform/parseExpr.js +2 -0
  66. package/lib/transform/transformUtilsNew.js +36 -9
  67. package/lib/transform/translateAssocsToJoins.js +11 -4
  68. package/lib/transform/universalCsn/coreComputed.js +15 -7
  69. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  70. package/package.json +2 -1
@@ -9,6 +9,7 @@ const { forEachDefinition, normalizeTypeRef } = require('../model/csnUtils');
9
9
  const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnricher');
10
10
  const { isBetaEnabled } = require('../base/model');
11
11
  const { ModelError } = require('../base/error');
12
+ const { makeMessageFunction } = require('../base/messages.js');
12
13
  const { typeParameters, specialFunctions } = require('../compiler/builtins');
13
14
  const { forEach } = require('../utils/objectUtils');
14
15
  const {
@@ -34,6 +35,8 @@ function csnToCdl( csn, options ) {
34
35
  const special$self = !csn?.definitions?.$self && '$self';
35
36
  timetrace.start('CDL rendering');
36
37
 
38
+ const msg = makeMessageFunction(csn, options, 'to.cdl');
39
+
37
40
  if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn')) {
38
41
  // Since the expander modifies the CSN, we need to clone it first or
39
42
  // toCdl can't guarantee that the input CSN is not modified.
@@ -214,7 +217,6 @@ function csnToCdl( csn, options ) {
214
217
 
215
218
  // Not part of if/else cascade, because it may be in postfix notation.
216
219
  if (ext.actions) {
217
- // TODO: Merge with renderActionsAndFunctions() -> requires removal of static `with actions`
218
220
  const childEnv = increaseIndent(env);
219
221
  let actions = '';
220
222
  forEach(ext.actions, (actionName, action) => {
@@ -260,8 +262,6 @@ function csnToCdl( csn, options ) {
260
262
  if (ext.enum)
261
263
  return 'enum ';
262
264
  if (ext.elements) { // enum/elements ambiguity -> look into elements
263
- // TODO: Check for `type` as well once this is supported by the parser (and identified as elements):
264
- // `extend E with { a = 'string'; b: String }`
265
265
  const isLikelyElement = Object.keys(ext.elements)
266
266
  .find(name => ext.elements[name].value !== undefined);
267
267
  if (isLikelyElement)
@@ -326,7 +326,7 @@ function csnToCdl( csn, options ) {
326
326
  const childEnv = increaseIndent(env);
327
327
  for (const name in ext.actions) {
328
328
  const action = ext.actions[name];
329
- result += renderAnnotationAssignmentsAndDocComment(action, childEnv) + childEnv.indent + quoteIdIfRequired(name);
329
+ result += renderAnnotationAssignmentsAndDocComment(action, childEnv) + childEnv.indent + quoteNonIdentifierOrKeyword(name);
330
330
  // Action parameter annotations
331
331
  if (action.params)
332
332
  result += renderAnnotateParamsInParentheses(action.params, childEnv);
@@ -361,7 +361,7 @@ function csnToCdl( csn, options ) {
361
361
  for (const name in elements) {
362
362
  const elem = elements[name];
363
363
  result += renderAnnotationAssignmentsAndDocComment(elem, childEnv);
364
- result += childEnv.indent + quoteIdIfRequired(name);
364
+ result += childEnv.indent + quoteNonIdentifierOrKeyword(name);
365
365
  if (elem.elements)
366
366
  result += renderAnnotateStatementElements(elem.elements, childEnv);
367
367
  if (elem.enum)
@@ -385,7 +385,7 @@ function csnToCdl( csn, options ) {
385
385
  let result = '(\n';
386
386
  const paramAnnotations = [];
387
387
  forEach(params, (paramName, param) => {
388
- paramAnnotations.push( renderAnnotationAssignmentsAndDocComment(param, childEnv) + childEnv.indent + quoteIdIfRequired(paramName) );
388
+ paramAnnotations.push( renderAnnotationAssignmentsAndDocComment(param, childEnv) + childEnv.indent + quoteNonIdentifierOrKeyword(paramName) );
389
389
  });
390
390
  result += `${paramAnnotations.join(',\n')}\n${env.indent})`;
391
391
  return result;
@@ -541,37 +541,32 @@ function csnToCdl( csn, options ) {
541
541
  * Returns the resulting source string.
542
542
  *
543
543
  * @param {string} elementName
544
- * @param {CSN.Element|CSN.Enum} elm
544
+ * @param {CSN.Element|CSN.Enum} element
545
545
  * @param {CdlRenderEnvironment} env
546
546
  */
547
- function renderElement( elementName, elm, env ) {
547
+ function renderElement( elementName, element, env ) {
548
548
  env = envAddPath(env, [ 'elements', elementName ]);
549
- let result = renderAnnotationAssignmentsAndDocComment(elm, env);
549
+ let result = renderAnnotationAssignmentsAndDocComment(element, env);
550
550
  result += env.indent;
551
- result += elm.virtual ? 'virtual ' : '';
552
- result += elm.key ? 'key ' : '';
551
+ result += element.virtual ? 'virtual ' : '';
552
+ result += element.key ? 'key ' : '';
553
553
  // TODO(v4): Remove once deprecated flag for `masked` is removed.
554
- result += elm.masked ? 'masked ' : '';
555
- result += quoteIdIfRequired(elementName);
556
- if (elm.val !== undefined) { // enum value
557
- result += ` = ${exprRenderer.renderExpr(elm, env)}`;
554
+ result += element.masked ? 'masked ' : '';
555
+ result += quoteNonIdentifierOrKeyword(elementName);
556
+ if (element.val !== undefined) { // enum value
557
+ result += ` = ${exprRenderer.renderExpr(element, env)}`;
558
558
  }
559
- else if (elm['#'] !== undefined) { // enum symbol reference
560
- result += ` = #${elm['#']}`;
559
+ else if (element['#'] !== undefined) { // enum symbol reference
560
+ result += ` = #${element['#']}`;
561
561
  }
562
562
  else {
563
- // TODO: Maybe push the .value check into renderTypeReferenceAndProps?
564
- const type = normalizeTypeRef(elm.items?.type || elm.type);
565
- const isAssoc = type === 'cds.Association' || type === 'cds.Composition';
566
- if (!isAssoc || elm.value === undefined) {
567
- const props = renderTypeReferenceAndProps(elm, env);
568
- if (props !== '')
569
- result += ` : ${props}`;
570
- }
563
+ const props = renderTypeReferenceAndProps(element, env);
564
+ if (props !== '')
565
+ result += ` : ${props}`;
571
566
  }
572
567
 
573
- if (elm.value !== undefined) { // calculated element // @ts-ignore
574
- result += ` = ${exprRenderer.renderExpr(elm.value, env)}`;
568
+ if (element.value !== undefined) { // calculated element // @ts-ignore
569
+ result += ` = ${exprRenderer.renderExpr(element.value, env)}`;
575
570
  }
576
571
 
577
572
  return `${result};\n`;
@@ -697,7 +692,8 @@ function csnToCdl( csn, options ) {
697
692
  function renderViewSource( source, env ) {
698
693
  // Sub-SELECT
699
694
  if (source.SELECT || source.SET) {
700
- let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`;
695
+ const subEnv = increaseIndent(env);
696
+ let result = `(\n${subEnv.indent}${renderQuery(source, false, 'view', subEnv)}\n${env.indent})`;
701
697
  if (source.as)
702
698
  result += renderAlias(source.as);
703
699
 
@@ -762,9 +758,13 @@ function csnToCdl( csn, options ) {
762
758
  result += `(${renderArguments(path.ref[0], ':', env)})`;
763
759
 
764
760
  if (path.ref[0].where) {
761
+ // TODO: Unify with other filter rendering
765
762
  const cardinality = path.ref[0].cardinality ? (`${path.ref[0].cardinality.max}: `) : '';
766
763
  const expr = exprRenderer.renderExpr(path.ref[0].where, env);
767
- result += `[${cardinality}${expr}]`;
764
+ if (expr.endsWith(']')) // for cases such as [… ![id] ]
765
+ result += `[ ${cardinality}${expr} ]`;
766
+ else
767
+ result += `[${cardinality}${expr}]`;
768
768
  }
769
769
 
770
770
  // Add any path steps (possibly with parameters and filters) that may follow after that
@@ -827,7 +827,7 @@ function csnToCdl( csn, options ) {
827
827
  result += renderDocComment(element, env);
828
828
  }
829
829
  // Note: parentheses are a workaround for #9015
830
- result += renderAnnotationAssignmentsAndDocComment(col, env, { parens: true });
830
+ result += renderAnnotationAssignmentsAndDocComment(col, env, { parentheses: true });
831
831
  result += env.indent;
832
832
 
833
833
  // only if column is virtual, keyword virtual was present in the source text
@@ -1004,7 +1004,8 @@ function csnToCdl( csn, options ) {
1004
1004
  }
1005
1005
 
1006
1006
  if (select.excluding) {
1007
- result += ` excluding {\n${select.excluding.map(id => `${childEnv.indent}${quoteIdIfRequired(id)}`).join(',\n')}\n`;
1007
+ const excludes = select.excluding.map(id => `${childEnv.indent}${quoteNonIdentifierOrKeyword(id)}`).join(',\n');
1008
+ result += ` excluding {\n${excludes}\n`;
1008
1009
  result += `${env.indent}}`;
1009
1010
  }
1010
1011
 
@@ -1178,7 +1179,7 @@ function csnToCdl( csn, options ) {
1178
1179
  function renderParameter( parName, par, env ) {
1179
1180
  env = envAddPath(env, [ 'params', parName ]);
1180
1181
  let result = `${renderAnnotationAssignmentsAndDocComment(par, env)}${env.indent}`;
1181
- result += `${quoteIdIfRequired(parName)} : ${renderTypeReferenceAndProps(par, env)}`;
1182
+ result += `${quoteNonIdentifierOrKeyword(parName)} : ${renderTypeReferenceAndProps(par, env)}`;
1182
1183
  return result;
1183
1184
  }
1184
1185
 
@@ -1211,7 +1212,7 @@ function csnToCdl( csn, options ) {
1211
1212
  * such as `not null` and `default <xpr>`.
1212
1213
  * Allow suppressing rendering of structs such as enums - used in columns for example.
1213
1214
  *
1214
- * @param {object} artifact
1215
+ * @param {CSN.Artifact} artifact
1215
1216
  * @param {CdlRenderEnvironment} env
1216
1217
  * @param {object} [config={}] - `typeRefOnly` Whether to only render type defs, no arrayed/structured/enum.
1217
1218
  * - `noAnnoCollect` Do not collect annotations of sub-elements.
@@ -1220,7 +1221,6 @@ function csnToCdl( csn, options ) {
1220
1221
  function renderTypeReferenceAndProps( artifact, env, config = {} ) {
1221
1222
  let result = '';
1222
1223
  const { typeRefOnly, noAnnoCollect } = config;
1223
- let isTypeDef = env.path?.length === 2; // e.g [ 'definitions', typeDef ];
1224
1224
 
1225
1225
  if (typeRefOnly && !artifact.type)
1226
1226
  throw new ModelError(`Expected artifact to have a type; in: ${env.artifactName}`);
@@ -1229,20 +1229,16 @@ function csnToCdl( csn, options ) {
1229
1229
  result += 'localized ';
1230
1230
 
1231
1231
  if (!artifact.type && artifact.items) {
1232
+ checkArrayedArtifact(artifact, env);
1232
1233
  result += 'many '; // alternative: 'array of'; but not used
1233
- artifact = artifact.items;
1234
- env = envAddPath(env, 'items');
1235
- // element keywords allowed in MANY case; was an oversight when arrays were introduced.
1236
- isTypeDef = false;
1237
- // "many many" does not work in CDL, so we don't check for it.
1234
+ ({ art: artifact, env } = checkInnerMostArray(artifact, env));
1238
1235
  }
1239
1236
 
1240
1237
  const type = normalizeTypeRef(artifact.type);
1241
1238
 
1242
1239
  if (!type && artifact.elements) {
1243
1240
  result += renderElements(artifact, env);
1244
- if (!isTypeDef)
1245
- result += renderNullability(artifact);
1241
+ result += renderNullability(artifact);
1246
1242
  // structured default not possible at the moment
1247
1243
  return result;
1248
1244
  }
@@ -1278,7 +1274,7 @@ function csnToCdl( csn, options ) {
1278
1274
  if (artifact.keys && !artifact.on)
1279
1275
  result += ` { ${Object.keys(artifact.keys).map(name => renderForeignKey(artifact.keys[name], env)).join(', ')} }`;
1280
1276
 
1281
- if (!isTypeDef && !artifact.on) // unmanaged associations can't be followed by "not null"
1277
+ if (artifact.notNull !== undefined && !artifact.on) // unmanaged associations can't be followed by "not null"
1282
1278
  result += renderNullability(artifact);
1283
1279
  // DEFAULT not possible here.
1284
1280
 
@@ -1291,8 +1287,6 @@ function csnToCdl( csn, options ) {
1291
1287
  // get lost if we only render the type name.
1292
1288
  // We only extract annotations of enums, if "typeRefOnly" is true. Otherwise, since
1293
1289
  // the full enum is rendered below, we would have unnecessary annotations.
1294
- // TODO: Can we annotate elements of targetAspect?
1295
- // If so, move this block before the composition rendering.
1296
1290
  if (!noAnnoCollect && (!artifact.enum || typeRefOnly)) {
1297
1291
  const annotate = collectAnnotationsOfElementsAndEnum(artifact, env);
1298
1292
  if (annotate)
@@ -1310,9 +1304,9 @@ function csnToCdl( csn, options ) {
1310
1304
 
1311
1305
  if (artifact.enum && !typeRefOnly)
1312
1306
  result += renderEnum(artifact.enum, env);
1313
- if (!isTypeDef) // NOT NULL not possible for not-arrayed type definitions
1307
+ if (artifact.notNull !== undefined)
1314
1308
  result += renderNullability(artifact);
1315
- if (artifact.default)
1309
+ if (artifact.default !== undefined)
1316
1310
  result += ` default ${exprRenderer.renderExpr(artifact.default, env)}`;
1317
1311
 
1318
1312
  return result;
@@ -1388,56 +1382,86 @@ function csnToCdl( csn, options ) {
1388
1382
  * @param {CdlRenderEnvironment} env
1389
1383
  */
1390
1384
  function renderAnnotationValue( annoValue, env ) {
1385
+ // TODO: There must be at least one known expression property, otherwise
1386
+ // it could be `type: 'unchecked'`.
1391
1387
  const isXpr = annoValue?.['='] !== undefined && (Object.keys(annoValue).length > 1) &&
1392
1388
  isBetaEnabled(options, 'annotationExpressions');
1393
1389
  if (isXpr) {
1390
+ // Once inside an expression, we stay there.
1394
1391
  const xpr = exprRenderer.renderExpr(annoValue, env);
1395
1392
  return `( ${xpr} )`;
1396
1393
  }
1397
- return renderSimpleAnnotationValue(annoValue, env);
1398
- }
1399
-
1400
- function renderSimpleAnnotationValue( x, env ) {
1401
- if (Array.isArray(x)) {
1402
- // Render array parts as values. Spaces required if last array value is
1403
- // a delimited identifier.
1404
- return `[ ${x.map(item => renderSimpleAnnotationValue(item, env)).join(', ')} ]`;
1394
+ else if (Array.isArray(annoValue)) {
1395
+ return renderAnnotationArrayValue( annoValue, env );
1405
1396
  }
1406
- else if (typeof x === 'object' && x !== null) {
1397
+ else if (typeof annoValue === 'object' && annoValue !== null) {
1407
1398
  // Enum symbol
1408
- if (x['#']) {
1409
- return `#${x['#']}`;
1399
+ if (annoValue['#']) {
1400
+ return `#${annoValue['#']}`;
1410
1401
  }
1411
1402
  // Shorthand for absolute path (as string)
1412
- else if (x['=']) {
1413
- return quotePathIfRequired(x['=']);
1403
+ else if (annoValue['=']) {
1404
+ if (annoValue['='].startsWith('@'))
1405
+ return quoteAnnotationPathIfRequired(annoValue['=']);
1406
+ return quotePathIfRequired(annoValue['=']);
1414
1407
  }
1415
1408
  // Shorthand for ellipsis: `... up to <val>`
1416
- else if (x['...']) {
1417
- if (x['...'] === true)
1409
+ else if (annoValue['...']) {
1410
+ if (annoValue['...'] === true)
1418
1411
  return '...';
1419
- return `... up to ${renderSimpleAnnotationValue(x['...'], env)}`;
1412
+ return `... up to ${renderAnnotationValue(annoValue['...'], env)}`;
1420
1413
  }
1421
1414
 
1422
1415
  // Struct value (can currently only occur within an array)
1423
1416
  // Render as one-liner if there is at most one key. Render as multi-line
1424
1417
  // struct if there are more and use nicer indentation.
1425
- const keys = Object.keys(x);
1418
+ const keys = Object.keys(annoValue);
1426
1419
  const childEnv = keys.length <= 1 ? env : increaseIndent(env);
1427
- const values = keys.map(key => `${quoteAnnotationPathIfRequired(key)}: ${renderSimpleAnnotationValue(x[key], childEnv)}`);
1420
+ const values = keys.map(key => `${quoteAnnotationPathIfRequired(key)}: ${renderAnnotationValue(annoValue[key], childEnv)}`);
1428
1421
  if (values.length <= 1)
1429
1422
  return `{ ${values.join(', ')} }`;
1430
1423
  const valueList = values.join(`,\n${childEnv.indent}`);
1431
1424
  return `{\n${childEnv.indent}${valueList}\n${env.indent}}`;
1432
1425
  }
1433
1426
  // Null
1434
- else if (x === null) {
1427
+ else if (annoValue === null) {
1435
1428
  return 'null';
1436
1429
  }
1437
1430
  // Primitive: string, number, boolean
1438
1431
 
1439
1432
  // Quote strings, leave all others as they are
1440
- return (typeof x === 'string') ? renderString(x, env) : x;
1433
+ return (typeof annoValue === 'string') ? renderString(annoValue, env) : String(annoValue);
1434
+ }
1435
+
1436
+ /**
1437
+ * Renders an array annotation value. Uses a heuristic to put each element on its own line
1438
+ * if a single-line becomes longer than 100 characters or if any sub-expression already
1439
+ * contains a line break. The latter checks makes nested arrays with structures more
1440
+ * readable.
1441
+ *
1442
+ * @param {any[]} annoValue
1443
+ * @param {CdlRenderEnvironment} env
1444
+ * @return {string}
1445
+ */
1446
+ function renderAnnotationArrayValue( annoValue, env ) {
1447
+ const childEnv = increaseIndent(env);
1448
+ // Render array parts as values.
1449
+ let length = 0;
1450
+ let hasLineBreak = false;
1451
+ const items = annoValue.map((item) => {
1452
+ const result = renderAnnotationValue(item, childEnv);
1453
+ length += result.length + 2; // just a heuristic; add 2 for `, `.
1454
+ if (!hasLineBreak && result.includes('\n'))
1455
+ hasLineBreak = true;
1456
+ return result;
1457
+ });
1458
+
1459
+ if (!hasLineBreak && (length + env.indent.length) < 100) {
1460
+ // Spaces required if last array value is a delimited identifier.
1461
+ return `[ ${items.join(', ')} ]`;
1462
+ }
1463
+ const renderedItems = items.join(`,\n${childEnv.indent}`);
1464
+ return `[\n${childEnv.indent}${renderedItems}\n${env.indent}]`;
1441
1465
  }
1442
1466
 
1443
1467
  /**
@@ -1455,7 +1479,7 @@ function csnToCdl( csn, options ) {
1455
1479
  // FIXME: We should rather explicitly recognize quoting somehow
1456
1480
  if (idx === 0 && s.startsWith('$'))
1457
1481
  return s;
1458
- return quoteIdIfRequired(s, env.additionalKeywords);
1482
+ return quoteNonIdentifierOrKeyword(s, env.additionalKeywords);
1459
1483
  }
1460
1484
  // ID with filters or parameters
1461
1485
  else if (typeof s === 'object') {
@@ -1468,7 +1492,7 @@ function csnToCdl( csn, options ) {
1468
1492
  return `${s.func}(${renderArguments(s, '=>', env)})`;
1469
1493
 
1470
1494
  // Path step, possibly with view parameters and/or filters
1471
- let result = `${quoteIdIfRequired(s.id, env.additionalKeywords)}`;
1495
+ let result = `${quoteNonIdentifierOrKeyword(s.id, env.additionalKeywords)}`;
1472
1496
  if (s.args) {
1473
1497
  // View parameters
1474
1498
  result += `(${renderArguments(s, ':', env)})`;
@@ -1477,7 +1501,10 @@ function csnToCdl( csn, options ) {
1477
1501
  // Filter, possibly with cardinality
1478
1502
  const cardinality = s.cardinality ? (`${s.cardinality.max}: `) : '';
1479
1503
  const expr = exprRenderer.renderExpr(s.where, env);
1480
- result += `[${cardinality}${expr}]`;
1504
+ if (expr.endsWith(']')) // for cases such as [… ![id] ]
1505
+ result += `[ ${cardinality}${expr} ]`;
1506
+ else
1507
+ result += `[${cardinality}${expr}]`;
1481
1508
  }
1482
1509
 
1483
1510
  return result;
@@ -1516,7 +1543,7 @@ function csnToCdl( csn, options ) {
1516
1543
  */
1517
1544
  function renderNamedArguments( node, separator, env ) {
1518
1545
  return Object.keys(node.args).map(function renderNamedArgument(key) {
1519
- return `${quoteIdIfRequired(key, env.additionalKeywords)} ${separator} ${renderArgument(node.args[key], env)}`;
1546
+ return `${quoteNonIdentifierOrKeyword(key, env.additionalKeywords)} ${separator} ${renderArgument(node.args[key], env)}`;
1520
1547
  }).join(', ');
1521
1548
  }
1522
1549
 
@@ -1658,7 +1685,7 @@ function csnToCdl( csn, options ) {
1658
1685
  * @return {string}
1659
1686
  */
1660
1687
  function renderAlias( alias ) {
1661
- return ` as ${quoteIdIfRequired(alias)}`;
1688
+ return ` as ${quoteNonIdentifierOrKeyword(alias)}`;
1662
1689
  }
1663
1690
 
1664
1691
  /**
@@ -1696,7 +1723,7 @@ function csnToCdl( csn, options ) {
1696
1723
  *
1697
1724
  * @param {object} obj Object that has annotations
1698
1725
  * @param {CdlRenderEnvironment} env
1699
- * @param {{parens: boolean}} [config] Config for renderAnnotationAssignment()
1726
+ * @param {{parentheses: boolean}} [config] Config for renderAnnotationAssignment()
1700
1727
  * @return {string}
1701
1728
  */
1702
1729
  function renderAnnotationAssignmentsAndDocComment( obj, env, config ) {
@@ -1716,38 +1743,29 @@ function csnToCdl( csn, options ) {
1716
1743
  * @param {any} anno Annotation value
1717
1744
  * @param {string} name Annotation name, e.g. `@A.B.C#foo.C`
1718
1745
  * @param {CdlRenderEnvironment} env
1719
- * @param {object} [config] parens: Whether the annotation assignment must be surrounded by parentheses.
1746
+ * @param {object} [config] parentheses: Whether the annotation assignment must be surrounded by parentheses.
1720
1747
  * @return {string} Rendered annotation, possibly quoted: `@![A.B.C#foo.C]: value`
1721
1748
  */
1722
- function renderAnnotationAssignment( anno, name, env, config = { parens: false } ) {
1749
+ function renderAnnotationAssignment( anno, name, env, config = { parentheses: false } ) {
1723
1750
  name = name.substring(1);
1724
1751
  // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1725
1752
  const parts = name.split('#');
1726
1753
  const nameBeforeVariant = parts[0];
1727
1754
  const variant = parts[1];
1728
- const { parens } = config;
1729
-
1730
- // Identifier according to our grammar: /[$_a-zA-Z][$_a-zA-Z0-9]*/
1731
- // We expand this pattern to also include dots after the first character.
1732
- // If the annotation does not follow this pattern `ident(.@ident)*`, it must be quoted:
1733
- // `@identifier@identifier` must be quoted but `@identifier.@identifier` should not.
1734
- // TODO: Use quoteAnnotationPathIfRequired()
1735
- const annoRequiresQuoting = !/^[$_a-zA-Z][$_a-zA-Z0-9.]*(?:\.@[$_a-zA-Z][$_a-zA-Z0-9.]*)*$/.test(nameBeforeVariant);
1736
- // Unfortunately, the compiler does not allow `.` after the first variant identifier,
1737
- // even though that is the result after flattening.
1738
- const variantRequiresQuoting = variant && !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(variant);
1755
+ const { parentheses } = config;
1739
1756
 
1740
1757
  let result = `${env.indent}@`;
1741
- if (parens)
1758
+ if (parentheses)
1742
1759
  result += '(';
1743
1760
 
1744
- if (annoRequiresQuoting || variantRequiresQuoting)
1745
- result += delimitedId(name);
1746
- else
1747
- result += name;
1748
-
1761
+ result += quoteAnnotationPathIfRequired(nameBeforeVariant);
1762
+ if (variant !== undefined)
1763
+ // Unfortunately, the compiler does not allow `.@` after the first variant identifier,
1764
+ // so we're back at simple paths.
1765
+ result += `#${quotePathIfRequired(variant)}`;
1749
1766
  result += ` : ${renderAnnotationValue(anno, env)}`;
1750
- if (parens)
1767
+
1768
+ if (parentheses)
1751
1769
  result += ')';
1752
1770
  return `${result}\n`;
1753
1771
  }
@@ -1764,7 +1782,7 @@ function csnToCdl( csn, options ) {
1764
1782
 
1765
1783
  /**
1766
1784
  * Render the name of a definition. Ensures the first segment of the name
1767
- * is available in the rendered CDL. Otherwise a USING is added.
1785
+ * is available in the rendered CDL. Otherwise, a USING is added.
1768
1786
  *
1769
1787
  * @param {string} name
1770
1788
  * @return {string}
@@ -1846,6 +1864,62 @@ function csnToCdl( csn, options ) {
1846
1864
  },
1847
1865
  });
1848
1866
  }
1867
+
1868
+ // checks -------------------------------------------------------------------
1869
+ // The CDL backend has very few checks, but we need to tell the user if
1870
+ // something can't be rendered.
1871
+
1872
+ /**
1873
+ * to.cdl() can only render one nesting level of `items`. `items` inside `items, etc.
1874
+ * can't be represented in CDL, hence can't be rendered.
1875
+ * However, it's possible that due to CSN expansion because of type-ofs, we have
1876
+ * nested `.items` with a `.type` next to it. In that case, return the node with `.type`.
1877
+ *
1878
+ * Returns the most deeply nested `.items`. Upper bound are 100 nesting levels.
1879
+ *
1880
+ * @param {CSN.Artifact} art
1881
+ * @param {CdlRenderEnvironment} env
1882
+ * @return {{art: CSN.Artifact, env: CdlRenderEnvironment}} `art` and new `env` with adapted `env.path`.
1883
+ */
1884
+ function checkInnerMostArray( art, env ) {
1885
+ env = envNewPath(env, env.path); // copy path, so we can modify it directly
1886
+
1887
+ let nesting = 0;
1888
+ while (art.items && nesting < 100) {
1889
+ art = art.items;
1890
+ env.path.push( 'items');
1891
+ ++nesting;
1892
+ if (art.type)
1893
+ break; // after first `.items`, break at nesting level that has a type.
1894
+ }
1895
+
1896
+ if (nesting >= 100) {
1897
+ msg.error('def-invalid-nesting', env.path, { count: nesting, prop: 'items' },
1898
+ 'Property $(PROP) is nested more than $(COUNT) levels and can\'t be rendered');
1899
+ }
1900
+ else if (nesting > 1) {
1901
+ msg.warning('def-unexpected-nesting', env.path, { prop: 'items' },
1902
+ 'Property $(PROP) is nested more than one level; only rendering deepest level');
1903
+ }
1904
+ return { art, env };
1905
+ }
1906
+
1907
+ /**
1908
+ * If an artifact is an array via `.items`, some properties on `art` can't be rendered,
1909
+ * for example "not null", because there is no CDL representation for it. Only "not null"
1910
+ * on `.items` can be rendered.
1911
+ *
1912
+ * @param {CSN.Artifact} art
1913
+ * @param {CdlRenderEnvironment} env
1914
+ */
1915
+ function checkArrayedArtifact( art, env ) {
1916
+ if (!art.items)
1917
+ return;
1918
+ if (art.notNull !== undefined) {
1919
+ msg.warning('def-unexpected-nullability', env.path, { prop: 'not null', otherprop: 'items' },
1920
+ 'Property $(PROP) not rendered, because it can only be rendered inside $(OTHERPROP) for arrayed artifacts');
1921
+ }
1922
+ }
1849
1923
  }
1850
1924
 
1851
1925
  /**
@@ -1883,24 +1957,32 @@ function increaseIndent( env ) {
1883
1957
  }
1884
1958
 
1885
1959
  /**
1886
- * Quote the path steps with `![]` if necessary. For simple ids such as
1887
- * `elem` use `quoteIdIfRequired` instead.
1960
+ * Quote simple path steps with `![]` if necessary. For simple ids such as
1961
+ * `elem` use `quoteNonIdentifierOrKeyword` instead.
1888
1962
  *
1889
- * In contrast to quoteIdIfRequired, does not handle additional keywords,
1963
+ * In contrast to quoteNonIdentifierOrKeyword, does not handle additional keywords,
1890
1964
  * because it was not required, yet.
1891
1965
  *
1966
+ * Due to token rewrite, all keywords after a dot (`.`) are rewritten to
1967
+ * identifiers, i.e. we only need to check for the identifier RegEx.
1968
+ *
1892
1969
  * @param {string} path
1893
1970
  * @returns {string}
1894
- *
1895
- * @todo For paths such as `E.key`, `key` does not have to be in quotes.
1896
1971
  */
1897
1972
  function quotePathIfRequired( path ) {
1898
- return path.split('.').map(step => quoteIdIfRequired(step)).join('.');
1973
+ return path.split('.').map((step, index) => {
1974
+ if (index === 0)
1975
+ return quoteNonIdentifierOrKeyword(step);
1976
+ else if (!identifierRegex.test(step))
1977
+ return delimitedId(step);
1978
+ return step;
1979
+ }).join('.');
1899
1980
  }
1900
1981
 
1901
1982
  /**
1902
1983
  * Quote the id with `![]` if necessary. For paths such as `E.key` use
1903
1984
  * `quotePathIfRequired` instead.
1985
+ * See quoteNonIdentifier() if you want to ignore keywords.
1904
1986
  *
1905
1987
  * Set additionalKeywords to an array of UPPERCASE keywords
1906
1988
  * that also need quoting, e.g. in special functions.
@@ -1909,13 +1991,30 @@ function quotePathIfRequired( path ) {
1909
1991
  * @param {string[]} [additionalKeywords]
1910
1992
  * @return {string}
1911
1993
  */
1912
- function quoteIdIfRequired( id, additionalKeywords ) {
1994
+ function quoteNonIdentifierOrKeyword( id, additionalKeywords ) {
1913
1995
  // Quote if required for CDL
1914
1996
  if (requiresQuotingForCdl(id, additionalKeywords || []))
1915
1997
  return delimitedId(id);
1916
1998
  return id;
1917
1999
  }
1918
2000
 
2001
+ /**
2002
+ * Quote the id with `![]` if necessary. For paths such as `E.key` use
2003
+ * `quotePathIfRequired` instead.
2004
+ * See quoteNonIdentifierOrKeyword() if you want to quote identifiers
2005
+ * that are keywords as well.
2006
+ *
2007
+ * Does not quote the given id if it is a keyword.
2008
+ *
2009
+ * @param {string} id
2010
+ * @return {string}
2011
+ */
2012
+ function quoteNonIdentifier( id ) {
2013
+ if (!identifierRegex.test(id))
2014
+ return delimitedId(id);
2015
+ return id;
2016
+ }
2017
+
1919
2018
  /**
1920
2019
  * Quote an annotation path, e.g. `@My.@Anno.Description` if necessary.
1921
2020
  * `anno` can start with `@` but is not required to be.
@@ -1928,8 +2027,8 @@ function quoteIdIfRequired( id, additionalKeywords ) {
1928
2027
  function quoteAnnotationPathIfRequired( anno ) {
1929
2028
  return anno.split('.').map((segment) => {
1930
2029
  if (segment.startsWith('@'))
1931
- return `@${quoteIdIfRequired(segment.slice(1))}`;
1932
- return quoteIdIfRequired(segment);
2030
+ return `@${quoteNonIdentifier(segment.slice(1))}`;
2031
+ return quoteNonIdentifier(segment);
1933
2032
  }).join('.');
1934
2033
  }
1935
2034
 
@@ -2154,8 +2253,8 @@ function availableFirstPathSteps( csn ) {
2154
2253
  function smartId( id, insideFunction = null ) {
2155
2254
  insideFunction = insideFunction?.toUpperCase();
2156
2255
  if (!insideFunction || !specialFunctions[insideFunction])
2157
- return quoteIdIfRequired(id);
2158
- return quoteIdIfRequired(id, getAllKeywordsForSpecialFunction(insideFunction));
2256
+ return quoteNonIdentifierOrKeyword(id);
2257
+ return quoteNonIdentifierOrKeyword(id, getAllKeywordsForSpecialFunction(insideFunction));
2159
2258
  }
2160
2259
 
2161
2260
  /**
@@ -632,7 +632,6 @@ function toHdbcdsSource( csn, options ) {
632
632
  let result = `(${renderQuery(source, false, increaseIndent(env))})`;
633
633
  if (source.as)
634
634
  result += ` as ${formatIdentifier(source.as)}`;
635
-
636
635
  return result;
637
636
  }
638
637
  // JOIN