@sap/cds-compiler 3.1.2 → 3.4.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 (117) hide show
  1. package/CHANGELOG.md +101 -3
  2. package/bin/cdsc.js +4 -2
  3. package/doc/CHANGELOG_BETA.md +35 -0
  4. package/lib/api/main.js +153 -29
  5. package/lib/api/validate.js +8 -3
  6. package/lib/base/dictionaries.js +6 -6
  7. package/lib/base/error.js +2 -2
  8. package/lib/base/keywords.js +106 -24
  9. package/lib/base/message-registry.js +177 -79
  10. package/lib/base/messages.js +78 -57
  11. package/lib/base/model.js +2 -1
  12. package/lib/checks/actionsFunctions.js +1 -1
  13. package/lib/checks/annotationsOData.js +2 -2
  14. package/lib/checks/arrayOfs.js +15 -7
  15. package/lib/checks/cdsPersistence.js +1 -1
  16. package/lib/checks/checkForTypes.js +53 -0
  17. package/lib/checks/defaultValues.js +4 -2
  18. package/lib/checks/elements.js +81 -6
  19. package/lib/checks/foreignKeys.js +12 -13
  20. package/lib/checks/invalidTarget.js +10 -11
  21. package/lib/checks/managedInType.js +21 -15
  22. package/lib/checks/nullableKeys.js +1 -1
  23. package/lib/checks/onConditions.js +9 -9
  24. package/lib/checks/parameters.js +23 -0
  25. package/lib/checks/queryNoDbArtifacts.js +1 -1
  26. package/lib/checks/selectItems.js +1 -1
  27. package/lib/checks/sql-snippets.js +12 -10
  28. package/lib/checks/types.js +2 -2
  29. package/lib/checks/utils.js +17 -7
  30. package/lib/checks/validator.js +36 -14
  31. package/lib/compiler/assert-consistency.js +21 -13
  32. package/lib/compiler/builtins.js +8 -0
  33. package/lib/compiler/checks.js +57 -40
  34. package/lib/compiler/define.js +139 -69
  35. package/lib/compiler/extend.js +319 -50
  36. package/lib/compiler/finalize-parse-cdl.js +14 -9
  37. package/lib/compiler/kick-start.js +2 -35
  38. package/lib/compiler/populate.js +111 -68
  39. package/lib/compiler/propagator.js +5 -3
  40. package/lib/compiler/resolve.js +71 -108
  41. package/lib/compiler/shared.js +82 -54
  42. package/lib/compiler/tweak-assocs.js +26 -14
  43. package/lib/compiler/utils.js +13 -2
  44. package/lib/edm/annotations/genericTranslation.js +10 -7
  45. package/lib/edm/csn2edm.js +11 -11
  46. package/lib/edm/edm.js +17 -9
  47. package/lib/edm/edmPreprocessor.js +53 -30
  48. package/lib/edm/edmUtils.js +7 -2
  49. package/lib/gen/Dictionary.json +14 -0
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +3 -2
  52. package/lib/gen/languageParser.js +4312 -4186
  53. package/lib/inspect/inspectModelStatistics.js +1 -1
  54. package/lib/inspect/inspectPropagation.js +23 -9
  55. package/lib/json/csnVersion.js +13 -13
  56. package/lib/json/from-csn.js +161 -172
  57. package/lib/json/to-csn.js +70 -10
  58. package/lib/language/.eslintrc.json +4 -0
  59. package/lib/language/antlrParser.js +8 -11
  60. package/lib/language/docCommentParser.js +1 -2
  61. package/lib/language/errorStrategy.js +54 -27
  62. package/lib/language/genericAntlrParser.js +140 -93
  63. package/lib/language/language.g4 +57 -33
  64. package/lib/language/multiLineStringParser.js +75 -63
  65. package/lib/main.d.ts +3 -6
  66. package/lib/main.js +1 -0
  67. package/lib/model/.eslintrc.json +13 -0
  68. package/lib/model/api.js +4 -2
  69. package/lib/model/csnRefs.js +78 -50
  70. package/lib/model/csnUtils.js +272 -222
  71. package/lib/model/enrichCsn.js +41 -31
  72. package/lib/model/revealInternalProperties.js +61 -57
  73. package/lib/model/sortViews.js +35 -31
  74. package/lib/modelCompare/compare.js +52 -18
  75. package/lib/modelCompare/filter.js +83 -0
  76. package/lib/optionProcessor.js +10 -1
  77. package/lib/render/manageConstraints.js +11 -7
  78. package/lib/render/toCdl.js +151 -106
  79. package/lib/render/toHdbcds.js +8 -6
  80. package/lib/render/toRename.js +4 -4
  81. package/lib/render/toSql.js +17 -7
  82. package/lib/render/utils/common.js +27 -9
  83. package/lib/render/utils/sql.js +5 -5
  84. package/lib/sql-identifier.js +7 -0
  85. package/lib/transform/db/applyTransformations.js +32 -3
  86. package/lib/transform/db/assertUnique.js +27 -38
  87. package/lib/transform/db/expansion.js +92 -41
  88. package/lib/transform/db/flattening.js +1 -1
  89. package/lib/transform/db/temporal.js +3 -1
  90. package/lib/transform/db/transformExists.js +8 -2
  91. package/lib/transform/db/views.js +42 -13
  92. package/lib/transform/draft/db.js +2 -2
  93. package/lib/transform/forOdataNew.js +10 -7
  94. package/lib/transform/{forHanaNew.js → forRelationalDB.js} +18 -12
  95. package/lib/transform/localized.js +29 -20
  96. package/lib/transform/odata/toFinalBaseType.js +8 -11
  97. package/lib/transform/odata/typesExposure.js +2 -1
  98. package/lib/transform/parseExpr.js +245 -0
  99. package/lib/transform/transformUtilsNew.js +122 -51
  100. package/lib/transform/translateAssocsToJoins.js +17 -16
  101. package/lib/utils/moduleResolve.js +5 -5
  102. package/lib/utils/objectUtils.js +3 -3
  103. package/lib/utils/term.js +5 -5
  104. package/package.json +2 -2
  105. package/share/messages/anno-duplicate-unrelated-layer.md +6 -6
  106. package/share/messages/check-proper-type-of.md +4 -4
  107. package/share/messages/check-proper-type.md +2 -2
  108. package/share/messages/duplicate-autoexposed.md +4 -4
  109. package/share/messages/extend-repeated-intralayer.md +4 -5
  110. package/share/messages/extend-unrelated-layer.md +4 -4
  111. package/share/messages/message-explanations.json +3 -1
  112. package/share/messages/redirected-to-ambiguous.md +7 -6
  113. package/share/messages/redirected-to-complex.md +63 -0
  114. package/share/messages/redirected-to-unrelated.md +6 -5
  115. package/share/messages/rewrite-not-supported.md +4 -4
  116. package/share/messages/{syntax-expected-integer.md → syntax-expecting-integer.md} +4 -4
  117. package/share/messages/wildcard-excluding-one.md +37 -0
@@ -42,9 +42,10 @@ function hasErrors( messages ) {
42
42
  * @param {string} moduleName
43
43
  * @returns {boolean}
44
44
  */
45
- function hasNonDowngradableErrors( messages, moduleName ) {
46
- return messages && messages.some( m => m.severity === 'Error' &&
47
- (!m.messageId || !isDowngradable( m.messageId, moduleName )));
45
+ function hasNonDowngradableErrors( messages, moduleName, deprecatedDowngradable ) {
46
+ return messages &&
47
+ messages.some( m => m.severity === 'Error' &&
48
+ !isDowngradable( m.messageId, moduleName, deprecatedDowngradable ));
48
49
  }
49
50
 
50
51
  /**
@@ -54,10 +55,11 @@ function hasNonDowngradableErrors( messages, moduleName ) {
54
55
  *
55
56
  * @param {string} messageId
56
57
  * @param {string} moduleName
58
+ * @param {boolean} deprecatedDowngradable
57
59
  * @returns {boolean}
58
60
  */
59
- function isDowngradable( messageId, moduleName ) {
60
- if (!centralMessages[messageId])
61
+ function isDowngradable( messageId, moduleName, deprecatedDowngradable ) {
62
+ if (!messageId || !centralMessages[messageId])
61
63
  return false;
62
64
 
63
65
  const msg = centralMessages[messageId];
@@ -66,10 +68,12 @@ function isDowngradable( messageId, moduleName ) {
66
68
  // the module, it is NEVER downgradable.
67
69
  if (msg.errorFor && msg.errorFor.includes(moduleName))
68
70
  return false;
69
-
70
- return (msg.severity !== 'Error' ||
71
- msg.configurableFor === true || // useful with error for syntax variants
72
- msg.configurableFor && msg.configurableFor.includes( moduleName ));
71
+ if (msg.severity !== 'Error')
72
+ return true;
73
+ const { configurableFor } = msg;
74
+ return (Array.isArray( configurableFor ))
75
+ ? configurableFor.includes( moduleName )
76
+ : configurableFor && (configurableFor !== 'deprecated' || deprecatedDowngradable);
73
77
  }
74
78
 
75
79
  /**
@@ -136,14 +140,11 @@ class CompileMessage {
136
140
  this.location = location;
137
141
  this.$location = { ...this.location, address: undefined };
138
142
  this.validNames = null;
139
- if (home) // semantic location, e.g. 'entity:"E"/element:"x"'
140
- this.home = home;
143
+ this.home = home; // semantic location, e.g. 'entity:"E"/element:"x"'
141
144
  this.severity = severity;
142
- if (id)
143
- Object.defineProperty( this, 'messageId', { value: id } );
144
- // this.messageId = id; // ids not yet finalized
145
- if (moduleName)
146
- Object.defineProperty( this, '$module', { value: moduleName, configurable: true } );
145
+ Object.defineProperty( this, 'messageId', { value: id } );
146
+ // this.messageId = id; // ids not yet finalized
147
+ Object.defineProperty( this, '$module', { value: moduleName, configurable: true } );
147
148
  }
148
149
 
149
150
  toString() { // should have no argument...
@@ -176,7 +177,7 @@ const severitySpecs = {
176
177
  * @param {boolean} deprecatedDowngradable
177
178
  * @returns {MessageSeverity}
178
179
  */
179
- function reclassifiedSeverity(msg, options, moduleName, deprecatedDowngradable ) {
180
+ function reclassifiedSeverity( msg, options, moduleName, deprecatedDowngradable ) {
180
181
  const spec = centralMessages[msg.messageId] || { severity: msg.severity, configurableFor: null, errorFor: null };
181
182
  if (spec.severity === 'Error') {
182
183
  const { configurableFor } = spec;
@@ -353,7 +354,7 @@ function makeMessageFunction( model, options, moduleName = null ) {
353
354
 
354
355
  messages.push( msg );
355
356
  hasNewError = hasNewError || msg.severity === 'Error' &&
356
- !(options.testMode && msg.messageId && isDowngradable( msg.messageId, moduleName ));
357
+ !(options.testMode && isDowngradable( msg.messageId, moduleName, deprecatedDowngradable ));
357
358
  if (!hasMessageArray)
358
359
  console.error( messageString( msg ) );
359
360
  return msg;
@@ -499,7 +500,7 @@ function makeMessageFunction( model, options, moduleName = null ) {
499
500
  if (!messages || !messages.length)
500
501
  return;
501
502
  const hasError = options.testMode ? hasNonDowngradableErrors : hasErrors;
502
- if (hasError( messages, moduleName ))
503
+ if (hasError( messages, moduleName, deprecatedDowngradable ))
503
504
  throw new CompilationError( messages, options.attachValidNames && model );
504
505
  }
505
506
 
@@ -517,7 +518,7 @@ function makeMessageFunction( model, options, moduleName = null ) {
517
518
  // Re-set the module regardless of severity, since we reclassified it.
518
519
  Object.defineProperty( msg, '$module', { value: moduleName, configurable: true } );
519
520
  hasNewError = hasNewError || severity === 'Error' &&
520
- !(options.testMode && msg.messageId && isDowngradable( msg.messageId, moduleName ));
521
+ !(options.testMode && isDowngradable( msg.messageId, moduleName, deprecatedDowngradable ));
521
522
  }
522
523
  }
523
524
  }
@@ -605,7 +606,7 @@ function _check$Severities( id, moduleName, severity ) {
605
606
  return;
606
607
  }
607
608
  // now try whether the message could be something less than an Error in the module due to user wishes
608
- if (!isDowngradable( id, moduleName )) { // always an error in module
609
+ if (!isDowngradable( id, moduleName, true )) { // always an error in module
609
610
  if (severity !== 'Error')
610
611
  throw new CompilerAssertion( `Inconsistent severity: Expecting "Error", not "${severity}" for message ID "${id}" in module "${moduleName}"` );
611
612
  }
@@ -639,14 +640,12 @@ function _check$Texts( id, prop, value ) {
639
640
  throw new CompilerAssertion( `Different texts for the same message ID. Expecting "${expected}", not "${value}" for ID "${id}" and text variant "${prop}"`);
640
641
  }
641
642
 
642
- const quote = { // could be an option in the future
643
- name: n => `“${ n }”`,
644
- prop: p => `‘${ p }’`,
645
- file: f => `‘${ f }’`,
646
- code: c => `«${ c }»`,
647
- meta: m => `‹${ m }›`,
648
- // TODO: probably use keyword as function name, but its name would not have length 4 :-(
649
- word: w => w.toUpperCase(), // keyword
643
+ const quote = { // could be an option in the future
644
+ double: p => `“${ p }”`, // for names, including annotation names (with preceeding `@`)
645
+ single: p => `‘${ p }’`, // for other things cited from the model
646
+ angle: p => `‹${ p }›`, // for tokens like ‹Identifier›, and similar
647
+ number: p => p, // for numbers like 42
648
+ upper: p => p.toUpperCase(), // for keywords reported by ANTLR, use prop.single in v4
650
649
  }
651
650
 
652
651
  const paramsTransform = {
@@ -654,32 +653,38 @@ const paramsTransform = {
654
653
  name: quoted,
655
654
  id: quoted,
656
655
  alias: quoted,
657
- anno: a => (a.charAt(0) === '@' ? quote.name( a ) : quote.name( '@' + a )),
658
- delimited: n => '![' + n + ']',
659
- file: quote.file,
660
- prop: quote.prop,
661
- otherprop: quote.prop,
662
- code: quote.code,
663
- newcode: quote.code,
664
- kind: quote.meta,
665
- keyword: quote.word,
656
+ anno: a => (a.charAt(0) === '@' ? quote.double( a ) : quote.double( '@' + a )),
657
+ annos: anno => anno.map(paramsTransform.anno).join(', '),
658
+ delimited: n => '![' + n + ']', // TODO: use quote.single around?
659
+ file: quote.single,
660
+ prop: quote.single,
661
+ siblingprop: quote.single,
662
+ parentprop: quote.single,
663
+ otherprop: quote.single,
664
+ code: quote.single,
665
+ newcode: quote.single,
666
+ kind: quote.angle, // consider using text variants instead
667
+ keyword: quote.upper,
666
668
  // more complex convenience:
667
669
  names: transformManyWith( quoted ),
668
- number: n => n,
669
- line: l => l,
670
- col: c => c,
671
- literal: l => l,
670
+ number: quote.number,
671
+ line: quote.number,
672
+ col: quote.number,
673
+ value: n => (typeof n !== 'number' ? quote.single( n ) : quote.number( n )),
674
+ othervalue: n => (typeof n !== 'number' ? quote.single( n ) : quote.number( n )),
672
675
  art: transformArg,
673
676
  service: transformArg,
674
677
  sorted_arts: transformManyWith( transformArg, true ),
675
678
  target: transformArg,
679
+ source: transformArg,
676
680
  elemref: transformElementRef,
677
681
  type: transformArg,
678
682
  offending: tokenSymbol,
683
+ op: quote.single,
679
684
  expecting: transformManyWith( tokenSymbol ),
680
685
  // msg: m => m,
681
686
  $reviewed: ignoreTextTransform,
682
- version: quote.meta,
687
+ version: quote.single, // TODO delete: just use for OData $(VERSION), with version: 2.0
683
688
  };
684
689
 
685
690
  function ignoreTextTransform() {
@@ -702,20 +707,20 @@ function transformManyWith( t, sorted ) {
702
707
  }
703
708
 
704
709
  function quoted( name ) {
705
- return (name) ? quote.name( name ) : '<?>'; // TODO: failure in --test-mode, then remove
710
+ return (name) ? quote.double( name ) : quote.angle( '?' ); // TODO: failure in --test-mode, then remove
706
711
  }
707
712
 
708
713
  function tokenSymbol( token ) {
709
714
  if (token.match( /^[A-Z][A-Z]/ )) // keyword
710
- return quote.word( token );
715
+ return quote.upper( token );
711
716
  else if (token.match( /^[A-Z][a-z]/ )) // Number, Identifier, ...
712
- return quote.meta( token );
717
+ return quote.angle( token );
713
718
  if (token.startsWith("'") && token.endsWith("'")) // operator token symbol
714
- return quote.prop( token.slice( 1, -1 ));
719
+ return quote.single( token.slice( 1, -1 ));
715
720
  else if (token === '<EOF>')
716
- return quote.meta( token.slice( 1, -1 ) );
721
+ return quote.angle( 'EOF' );
717
722
  else
718
- return quote.code( token ); // should not happen
723
+ return quote.single( token ); // should not happen
719
724
  }
720
725
 
721
726
  /**
@@ -801,6 +806,7 @@ function messageText( texts, params, transform ) {
801
806
  }
802
807
 
803
808
  function replaceInString( text, params ) {
809
+ const usedParams = [ '#', '$reviewed' ];
804
810
  let pattern = /\$\(([A-Z_]+)\)/g;
805
811
  let parts = [];
806
812
  let start = 0;
@@ -808,11 +814,11 @@ function replaceInString( text, params ) {
808
814
  let prop = p[1].toLowerCase();
809
815
  parts.push( text.substring( start, p.index ),
810
816
  (prop in params ? params[prop] : p[0]) );
811
- delete params[prop];
817
+ usedParams.push(prop);
812
818
  start = pattern.lastIndex;
813
819
  }
814
820
  parts.push( text.substring( start ) );
815
- let remain = (params['#']) ? [] : Object.keys( params ).filter( n => params[n] );
821
+ let remain = (params['#']) ? [] : Object.keys( params ).filter( n => !usedParams.includes(n) );
816
822
  return (remain.length)
817
823
  ? parts.join('') + '; ' +
818
824
  remain.map( n => n.toUpperCase() + ' = ' + params[n] ).join(', ')
@@ -1024,6 +1030,7 @@ function compareMessage( a, b ) {
1024
1030
  c( aEnd.endLine, bEnd.endLine ) ||
1025
1031
  c( aEnd.endCol, bEnd.endCol ) ||
1026
1032
  c( homeSortName( a ), homeSortName( b ) ) ||
1033
+ // TODO: severities?
1027
1034
  c( a.message, b.message ) );
1028
1035
  }
1029
1036
  else if (!aFile && !bFile)
@@ -1107,7 +1114,7 @@ function shortArtName( art ) {
1107
1114
  const { name } = art;
1108
1115
  if ([ 'select', 'action', 'alias', 'param' ].every( n => name[n] == null || name[n] === 1 ) &&
1109
1116
  !name.absolute.includes(':'))
1110
- return quote.name( name.element ? `${ name.absolute }:${ name.element }` : name.absolute );
1117
+ return quote.double( name.element ? `${ name.absolute }:${ name.element }` : name.absolute );
1111
1118
  return artName( art );
1112
1119
  }
1113
1120
 
@@ -1172,7 +1179,9 @@ function homeNameForExtend( art ) {
1172
1179
 
1173
1180
  // Surrounding parent may be another extension.
1174
1181
  const parent = art._parent;
1175
- if (!parent)
1182
+ if (!parent && art.name.absolute)
1183
+ return kind + ':' + artName(removeBlock(art));
1184
+ else if (!parent)
1176
1185
  return kind + ':' + quoted(absoluteName);
1177
1186
 
1178
1187
  if (art.name.param && parent.params) {
@@ -1199,6 +1208,14 @@ function homeNameForExtend( art ) {
1199
1208
  }
1200
1209
  // This case should not happen, but just in case
1201
1210
  return kind + ':' + artName(parent);
1211
+
1212
+ /**
1213
+ * Remove `select` from the 'art's name to avoid `block:1` in artName().
1214
+ * @TODO: Refactor homeNameForExtend (and possibly artName) to get rid of this function
1215
+ **/
1216
+ function removeBlock(obj) {
1217
+ return { ...obj, name: { ...obj.name, select: null } };
1218
+ }
1202
1219
  }
1203
1220
 
1204
1221
  function constructSemanticLocationFromCsnPath(csnPath, model) {
@@ -1220,11 +1237,15 @@ function constructSemanticLocationFromCsnPath(csnPath, model) {
1220
1237
 
1221
1238
  let { query } = analyseCsnPath(csnPath, model, false);
1222
1239
 
1223
- // remove definitions
1224
- csnPath.shift();
1240
+ const dictName = csnPath.shift();
1241
+ const dict = model[dictName];
1225
1242
  const artifactName = csnPath.shift();
1226
- let currentThing = model.definitions[artifactName];
1227
- let result = `${ (currentThing && currentThing.kind) ? currentThing.kind : 'artifact' }:${ _quoted(artifactName) }`;
1243
+ let currentThing = dict ? dict[artifactName] : undefined;
1244
+ let result = `${ (currentThing?.kind)
1245
+ ? currentThing.kind
1246
+ : (dictName === 'vocabularies'
1247
+ ? 'annotation'
1248
+ : 'artifact') }:${ _quoted(artifactName) }`;
1228
1249
 
1229
1250
  if (!currentThing)
1230
1251
  return result;
package/lib/base/model.js CHANGED
@@ -26,8 +26,9 @@ const availableBetaFlags = {
26
26
  hanaAssocRealCardinality: true,
27
27
  mapAssocToJoinCardinality: true,
28
28
  ignoreAssocPublishingInUnion: true,
29
- nestedProjections: true,
30
29
  enableUniversalCsn: true,
30
+ postgres: true,
31
+ aspectWithoutElements: true,
31
32
  // disabled by --beta-mode
32
33
  nestedServices: false,
33
34
  odataOpenType: true,
@@ -22,7 +22,7 @@ function checkActionOrFunction(art, artName, prop, path) {
22
22
 
23
23
  const serviceName = this.csnUtils.getServiceName(artName);
24
24
  if (!serviceName)
25
- this.warning(null, path, `Functions and actions must be declared in a service`);
25
+ this.warning(null, path, {}, 'Functions and actions must be declared in a service');
26
26
 
27
27
  if (art.kind === 'entity') {
28
28
  for (const [ actName, act ] of Object.entries(art.actions)) {
@@ -36,7 +36,7 @@ function checkCoreMediaTypeAllowence(member) {
36
36
  */
37
37
  function checkAnalytics(member) {
38
38
  if (member['@Analytics.Measure'] && !member['@Aggregation.default']) {
39
- this.info(null, member.$path,
39
+ this.info(null, member.$path, {},
40
40
  'Annotation “@Analytics.Measure” expects “@Aggregation.default” to be assigned for the same element as well');
41
41
  }
42
42
  }
@@ -63,7 +63,7 @@ function checkReadOnlyAndInsertOnly(artifact, artifactName) {
63
63
  if (!this.csnUtils.getServiceName(artifactName))
64
64
  return;
65
65
  if (artifact.kind === 'entity' && artifact['@readonly'] && artifact['@insertonly'])
66
- this.warning(null, artifact.$path, 'Annotations “@readonly” and “@insertonly” can\'t be assigned in combination');
66
+ this.warning(null, artifact.$path, {}, 'Annotations “@readonly” and “@insertonly” can\'t be assigned in combination');
67
67
  }
68
68
 
69
69
  module.exports = {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { setProp } = require('../base/model');
4
+
3
5
  // Only to be used with validator.js - a correct `this` value needs to be provided!
4
6
 
5
7
  /**
@@ -24,20 +26,26 @@ function validateAssociationsInItems(member) {
24
26
  if (element.on) { // Unmanaged association
25
27
  // Unmanaged associations are always forbidden for now
26
28
  // TODO: Check if the on-condition only references things inside of the .items
27
- this.error(null, member.$path, 'Unmanaged associations in "array of" or "many" are not allowed');
29
+ this.error(null, member.$path, {}, 'Unmanaged associations in "array of" or "many" are not allowed');
28
30
  }
29
31
  }
30
32
  }
31
33
  }
32
34
  };
33
35
  if (this.artifact && ( this.artifact.kind === 'entity' || this.artifact.query ) && member && member.items && member.$path[2] === 'elements') {
34
- if (member.items.type && member.items.type.ref)
35
- validate(this.artifactRef(member.items.type));
36
-
37
- else if (member.items.type)
38
- validate(this.csn.definitions[member.items.type]);
39
- else
36
+ if (member.items.type) {
37
+ const type = member.items.type.ref
38
+ ? this.artifactRef(member.items.type)
39
+ : this.csn.definitions[member.items.type];
40
+ if (type && !type.$visited) {
41
+ setProp(type, '$visited', true);
42
+ validate(type);
43
+ delete type.$visited;
44
+ }
45
+ }
46
+ else {
40
47
  validate(member.items);
48
+ }
41
49
  }
42
50
  }
43
51
 
@@ -17,7 +17,7 @@ function validateCdsPersistenceAnnotation(artifact, artifactName, prop, path) {
17
17
  // TODO: Why not filter over persistenceAnnos, is shorter!
18
18
  const TableUdfCv = Object.keys(artifact).filter(p => persistenceAnnos.includes(p) && artifact[p]);
19
19
  if (TableUdfCv.length > 1)
20
- this.error(null, path, `Annotations ${ TableUdfCv.join(', ') } can't be used in combination`);
20
+ this.error(null, path, { annos: TableUdfCv }, 'Annotations $(ANNOS) can\'t be used in combination');
21
21
  }
22
22
  }
23
23
 
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { isPersistedOnDatabase } = require('../model/csnUtils.js');
4
+
5
+ /**
6
+ * Check that `cds.hana` types are not used - we don't support them for postgres
7
+ *
8
+ * @param {object} parent Object with a type
9
+ * @param {string} name Name of the type property on parent
10
+ * @param {Array} type type to check
11
+ * @param {CSN.Path} path
12
+ */
13
+ function checkForHanaTypes(parent, name, type, path) {
14
+ const artifact = this.csn.definitions[path[1]];
15
+ if (artifact.kind === 'entity' && isPersistedOnDatabase(artifact) && typeof parent.type === 'string' && parent.type.startsWith('cds.hana.')) {
16
+ this.error('ref-unexpected-hana-type', [ ...path, 'type' ], { type: 'cds.hana', value: this.options.sqlDialect },
17
+ 'Types in the $(TYPE) namespace can\'t be used with sqlDialect $(VALUE)');
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Check that `cds.UInt8` is not used - we don't have a clear idea how to represent it on postgres and h2
23
+ *
24
+ * @param {object} parent Object with a type
25
+ * @param {string} name Name of the type property on parent
26
+ * @param {Array} type type to check
27
+ * @param {CSN.Path} path
28
+ */
29
+ function CheckForUInt8(parent, name, type, path) {
30
+ const artifact = this.csn.definitions[path[1]];
31
+ if (artifact.kind === 'entity' && isPersistedOnDatabase(artifact) && parent.type === 'cds.UInt8') {
32
+ this.error('ref-unexpected-type', [ ...path, 'type' ], { type: 'cds.UInt8', value: this.options.sqlDialect },
33
+ 'Type $(TYPE) can\'t be used with sqlDialect $(VALUE)');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check types - specifically for postgres and h2
39
+ *
40
+ * @param {object} parent Object with a type
41
+ * @param {string} name Name of the type property on parent
42
+ * @param {Array} type type to check
43
+ * @param {CSN.Path} path
44
+ */
45
+ function checkTypes(parent, name, type, path) {
46
+ checkForHanaTypes.bind(this)(parent, name, type, path);
47
+ if (this.options.sqlDialect === 'postgres' || this.options.sqlDialect === 'h2')
48
+ CheckForUInt8.bind(this)(parent, name, type, path);
49
+ }
50
+
51
+ module.exports = {
52
+ type: checkTypes,
53
+ };
@@ -20,8 +20,10 @@ function validateDefaultValues(member, memberName, prop, path) {
20
20
  // consume all unary signs
21
21
  while (member.default.xpr[i] === '-' || member.default.xpr[i] === '+')
22
22
  i++;
23
+ // TODO: This check only counts the number of leading signs, not inbetween (e.g. 1 - - 1).
24
+ // The message also needs to be improved.
23
25
  if (i > 1)
24
- this.error(null, path, `Illegal number of unary '+/-' operators`);
26
+ this.error(null, path, {}, 'Illegal number of unary ‘+’/‘-’ operators');
25
27
  }
26
28
  }
27
29
  }
@@ -36,7 +38,7 @@ function validateDefaultValues(member, memberName, prop, path) {
36
38
  */
37
39
  function rejectParamDefaultsInHanaCds(member, memberName, prop, path) {
38
40
  if (member.default && prop === 'params' && this.options.transformation === 'hdbcds')
39
- this.error(null, path, 'Parameter default values are not supported in SAP HANA CDS');
41
+ this.error(null, path, {}, 'Parameter default values are not supported in SAP HANA CDS');
40
42
  }
41
43
 
42
44
  /**
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { forEachMember, forEachMemberRecursively } = require('../model/csnUtils');
3
+ const { forEachMember, forEachMemberRecursively, isBuiltinType } = require('../model/csnUtils');
4
4
  const { isGeoTypeName } = require('../compiler/builtins');
5
-
5
+ const { setProp } = require('../base/model');
6
6
  // Only to be used with validator.js - a correct `this` value needs to be provided!
7
7
 
8
8
  /**
@@ -42,13 +42,15 @@ function checkPrimaryKey(art) {
42
42
  { type: finalBaseType.type, name: elemFqName },
43
43
  'Type $(TYPE) can\'t be used as primary key in element $(NAME)');
44
44
  }
45
- else if (finalBaseType && this.csnUtils.isStructured(finalBaseType)) {
45
+ else if (finalBaseType && this.csnUtils.isStructured(finalBaseType) && !finalBaseType.$visited) {
46
+ setProp(finalBaseType, '$visited', true);
46
47
  forEachMemberRecursively(finalBaseType,
47
48
  (subMember, subMemberName) => checkIfPrimaryKeyIsOfGeoType
48
49
  .bind(this)(subMember,
49
50
  `${ elemFqName }/${ subMemberName }`,
50
51
  member.key || parentIsKey,
51
52
  member.$path));
53
+ delete finalBaseType.$visited;
52
54
  }
53
55
  }
54
56
  }
@@ -68,13 +70,15 @@ function checkPrimaryKey(art) {
68
70
  this.error(null, parentPath || member.$path, { name: elemFqName },
69
71
  'Array-like type in element $(NAME) can\'t be used as primary key');
70
72
  }
71
- else if (finalBaseType && this.csnUtils.isStructured(finalBaseType)) {
73
+ else if (finalBaseType && this.csnUtils.isStructured(finalBaseType) && !finalBaseType.$visited) {
74
+ setProp(finalBaseType, '$visited', true);
72
75
  forEachMemberRecursively(finalBaseType,
73
76
  (subMember, subMemberName) => checkIfPrimaryKeyIsArray
74
77
  .bind(this)(subMember,
75
78
  `${ elemFqName }/${ subMemberName }`,
76
79
  member.key || parentIsKey,
77
80
  member.$path));
81
+ delete finalBaseType.$visited;
78
82
  }
79
83
  }
80
84
  }
@@ -89,7 +93,7 @@ function checkPrimaryKey(art) {
89
93
  function checkVirtualElement(member) {
90
94
  if (member.virtual) {
91
95
  if (this.csnUtils.isAssociation(member.type)) { // or Composition ???
92
- this.error(null, member.$path, `Element can't be virtual and an association`);
96
+ this.error(null, member.$path, {}, 'Element can\'t be virtual and an association');
93
97
  }
94
98
  }
95
99
  }
@@ -135,4 +139,75 @@ function checkManagedAssoc(art) {
135
139
  }
136
140
  }
137
141
 
138
- module.exports = { checkPrimaryKey, checkVirtualElement, checkManagedAssoc };
142
+ /**
143
+ * All DB & OData flat mode must reject recursive type usages in entities
144
+ * 'items' break recursion as 'items' will turn into an NCLOB and the path
145
+ * prefix to 'items' can be flattend in the DB.
146
+ * In OData flat mode the first appearance of 'items' breaks out into structured
147
+ * mode producing (legal) recursive complex types.
148
+ *
149
+ * @param {CSN.Artifact} art The artifact
150
+ */
151
+ function checkRecursiveTypeUsage(art) {
152
+ const visit = (def) => {
153
+ const loc = def.$path;
154
+ // recursive types are allowed inside arrays
155
+ if (def.items)
156
+ return;
157
+ let { type } = def;
158
+ let prevType;
159
+ let isDeref = false;
160
+ if (type && !isBuiltinType(type) && !def.elements) {
161
+ do {
162
+ prevType = type;
163
+ // TODO: `type.ref.length > 1`, but OData backend must be tested first (#5144)
164
+ // e.g. `{ ref: [ "MyType" ] }`
165
+ if (type.ref) {
166
+ def = this.artifactRef(type);
167
+ isDeref = true;
168
+ }
169
+ else {
170
+ def = this.csn.definitions[type];
171
+ }
172
+ type = def.type;
173
+ } while (type && !isBuiltinType(type) && !def.items && !def.elements && prevType !== type);
174
+ }
175
+ if (def.$visited || (type && prevType === type)) {
176
+ // Recursion via type is allowed in V4 struct, but not via dereferencing
177
+ if (!isDeref && this.options.odataVersion === 'v4' && this.options.odataFormat === 'structured')
178
+ return;
179
+ if (!def.$recErr) {
180
+ this.error(null, loc, {}, 'Unexpected recursive type definition');
181
+ setProp(def, '$recErr', true);
182
+ }
183
+ }
184
+ else if (def.elements) {
185
+ setProp(def, '$visited', true);
186
+ for (const n in def.elements)
187
+ visit(def.elements[n]);
188
+ delete def.$visited;
189
+ }
190
+ };
191
+ // elements & params are flattening candidates
192
+ // FUTURE:
193
+ // Once we have universal CSN for the runtimes
194
+ // Validate service members only for OData
195
+ if (art.kind === 'entity') {
196
+ for (const n in art.elements)
197
+ visit(art.elements[n]);
198
+ for (const n in art.params)
199
+ visit(art.params[n]);
200
+ }
201
+ if (this.options.odataVersion) {
202
+ // func/action params/returns don't allow recursive type derefs
203
+ if (art.kind === 'action' || art.kind === 'function') {
204
+ for (const n in art.params)
205
+ visit(art.params[n]);
206
+ if (art.returns)
207
+ visit(art.returns);
208
+ }
209
+ }
210
+ }
211
+ module.exports = {
212
+ checkPrimaryKey, checkVirtualElement, checkManagedAssoc, checkRecursiveTypeUsage,
213
+ };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { setProp } = require('../base/model');
4
+
3
5
  // Only to be used with validator.js - a correct this value needs to be provided!
4
6
 
5
7
  /**
@@ -41,10 +43,10 @@ function validateForeignKeys(member) {
41
43
  // Declared as arrow-function to keep scope the same (this value)
42
44
  const checkForItems = (mem) => {
43
45
  if (mem.items) {
44
- this.error(null, member.$path, 'Array-like properties must not be foreign keys');
46
+ this.error(null, member.$path, {}, 'Array-like properties must not be foreign keys');
45
47
  }
46
48
  else if (isUnmanagedAssoc(mem)) {
47
- this.error(null, member.$path, 'Unmanaged association must not be a foreign key');
49
+ this.error(null, member.$path, {}, 'Unmanaged association must not be a foreign key');
48
50
  }
49
51
  else if (mem.keys) {
50
52
  handleAssociation(mem);
@@ -52,17 +54,14 @@ function validateForeignKeys(member) {
52
54
  else if (mem.elements) {
53
55
  handleStructured(mem);
54
56
  }
55
- else if (mem.type && mem.type.ref) { // type of
56
- checkForItems(this.artifactRef(mem.type));
57
- }
58
- else { // type T where T might contain items
59
- const type = this.csn.definitions[mem.type];
60
- if (type) {
61
- if (type.keys)
62
- handleAssociation(type);
63
-
64
- else if (type.elements)
65
- handleStructured(type);
57
+ else if (mem.type) {
58
+ const type = mem.type.ref
59
+ ? this.artifactRef(mem.type)
60
+ : this.csn.definitions[mem.type];
61
+ if (type && !type.$visited) {
62
+ setProp(type, '$visited', true);
63
+ checkForItems(type);
64
+ delete type.$visited;
66
65
  }
67
66
  }
68
67
  };