@sap/cds-compiler 4.9.2 → 5.0.6

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 (88) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/bin/cds_remove_invalid_whitespace.js +2 -1
  3. package/bin/cdsc.js +15 -11
  4. package/bin/cdshi.js +1 -0
  5. package/doc/CHANGELOG_BETA.md +7 -0
  6. package/lib/api/main.js +7 -19
  7. package/lib/api/options.js +5 -11
  8. package/lib/api/trace.js +0 -1
  9. package/lib/base/builtins.js +1 -0
  10. package/lib/base/location.js +4 -1
  11. package/lib/base/message-registry.js +29 -29
  12. package/lib/base/messages.js +22 -26
  13. package/lib/base/model.js +0 -2
  14. package/lib/base/node-helpers.js +0 -1
  15. package/lib/checks/enricher.js +1 -5
  16. package/lib/checks/structuredAnnoExpressions.js +30 -0
  17. package/lib/checks/validator.js +8 -0
  18. package/lib/compiler/assert-consistency.js +4 -1
  19. package/lib/compiler/base.js +1 -1
  20. package/lib/compiler/builtins.js +18 -2
  21. package/lib/compiler/checks.js +2 -5
  22. package/lib/compiler/define.js +7 -7
  23. package/lib/compiler/extend.js +68 -33
  24. package/lib/compiler/generate.js +1 -1
  25. package/lib/compiler/index.js +23 -6
  26. package/lib/compiler/lsp-api.js +501 -2
  27. package/lib/compiler/populate.js +2 -2
  28. package/lib/compiler/propagator.js +1 -4
  29. package/lib/compiler/resolve.js +2 -15
  30. package/lib/compiler/shared.js +112 -31
  31. package/lib/compiler/tweak-assocs.js +2 -16
  32. package/lib/compiler/utils.js +2 -1
  33. package/lib/compiler/xsn-model.js +4 -0
  34. package/lib/edm/annotations/genericTranslation.js +95 -42
  35. package/lib/edm/csn2edm.js +16 -4
  36. package/lib/edm/edm.js +2 -3
  37. package/lib/edm/edmAnnoPreprocessor.js +1 -2
  38. package/lib/edm/edmPreprocessor.js +1 -7
  39. package/lib/gen/Dictionary.json +29 -2
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +2 -1
  42. package/lib/gen/languageParser.js +4995 -4817
  43. package/lib/json/csnVersion.js +1 -1
  44. package/lib/json/from-csn.js +4 -7
  45. package/lib/json/to-csn.js +23 -12
  46. package/lib/language/antlrParser.js +2 -2
  47. package/lib/language/errorStrategy.js +0 -1
  48. package/lib/language/genericAntlrParser.js +35 -12
  49. package/lib/language/multiLineStringParser.js +3 -2
  50. package/lib/language/textUtils.js +1 -0
  51. package/lib/main.d.ts +28 -9
  52. package/lib/main.js +7 -4
  53. package/lib/model/csnRefs.js +20 -4
  54. package/lib/model/csnUtils.js +0 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +1 -1
  57. package/lib/optionProcessor.js +28 -9
  58. package/lib/render/manageConstraints.js +1 -1
  59. package/lib/render/toCdl.js +36 -7
  60. package/lib/render/toSql.js +1 -0
  61. package/lib/render/utils/common.js +12 -9
  62. package/lib/render/utils/stringEscapes.js +1 -0
  63. package/lib/transform/db/applyTransformations.js +13 -8
  64. package/lib/transform/db/associations.js +62 -54
  65. package/lib/transform/db/constraints.js +23 -25
  66. package/lib/transform/db/expansion.js +1 -6
  67. package/lib/transform/db/flattening.js +89 -111
  68. package/lib/transform/db/temporal.js +3 -4
  69. package/lib/transform/db/views.js +0 -1
  70. package/lib/transform/draft/odata.js +51 -3
  71. package/lib/transform/effective/annotations.js +3 -2
  72. package/lib/transform/effective/flattening.js +135 -0
  73. package/lib/transform/effective/main.js +6 -6
  74. package/lib/transform/effective/types.js +13 -9
  75. package/lib/transform/forOdata.js +0 -2
  76. package/lib/transform/forRelationalDB.js +0 -19
  77. package/lib/transform/localized.js +7 -8
  78. package/lib/transform/odata/flattening.js +39 -31
  79. package/lib/transform/odata/typesExposure.js +5 -17
  80. package/lib/transform/transformUtils.js +1 -1
  81. package/lib/transform/translateAssocsToJoins.js +21 -3
  82. package/lib/utils/file.js +13 -7
  83. package/lib/utils/moduleResolve.js +59 -8
  84. package/lib/utils/term.js +3 -2
  85. package/package.json +7 -3
  86. package/share/messages/message-explanations.json +2 -0
  87. package/share/messages/type-unexpected-foreign-keys.md +52 -0
  88. package/share/messages/type-unexpected-on-condition.md +52 -0
@@ -8,8 +8,8 @@ const {
8
8
  forEachInOrder,
9
9
  forEachDefinition,
10
10
  forEachMember,
11
+ forEachGeneric,
11
12
  isDeprecatedEnabled,
12
- isBetaEnabled,
13
13
  } = require('../base/model');
14
14
  const { dictAdd, pushToDict } = require('../base/dictionaries');
15
15
  const { kindProperties, dictKinds } = require('./base');
@@ -30,7 +30,7 @@ const { Location } = require('../base/location');
30
30
 
31
31
  const $location = Symbol.for( 'cds.$location' );
32
32
 
33
- // attach stupid location - TODO: remove in v5
33
+ // attach stupid location - TODO: remove in v6
34
34
  const genLocation = new Location( '' );
35
35
 
36
36
  const draftElements = [
@@ -59,6 +59,7 @@ function extend( model ) {
59
59
  resolvePath,
60
60
  resolveUncheckedPath,
61
61
  resolveTypeArgumentsUnchecked,
62
+ resolveDefinitionName,
62
63
  attachAndEmitValidNames,
63
64
  initMembers,
64
65
  initSelectItems,
@@ -72,7 +73,6 @@ function extend( model ) {
72
73
  } );
73
74
 
74
75
  const includesNonShadowedFirst = isDeprecatedEnabled( model.options, 'includesNonShadowedFirst' );
75
- const isV5preview = isBetaEnabled( model.options, 'v5preview' );
76
76
 
77
77
  sortModelSources();
78
78
  const extensionsDict = Object.create( null ); // TODO TMP
@@ -161,6 +161,7 @@ function extend( model ) {
161
161
  moveReturnsExtensions( art, extensionsMap );
162
162
 
163
163
  if (art.returns) {
164
+ ensureArtifactNotProcessed( art.returns );
164
165
  pushToDict( art.returns, '_extensions', ...extensionsMap.elements || [] );
165
166
  pushToDict( art.returns, '_extensions', ...extensionsMap.enum || [] );
166
167
  if (art.kind !== 'annotate') {
@@ -171,16 +172,35 @@ function extend( model ) {
171
172
  }
172
173
  const sub = art.items || art.targetAspect?.elements && art.targetAspect;
173
174
  if (sub) {
175
+ ensureArtifactNotProcessed( sub );
174
176
  pushToDict( sub, '_extensions', ...extensionsMap.elements || [] );
175
177
  pushToDict( sub, '_extensions', ...extensionsMap.enum || [] );
176
178
  }
177
179
  else {
178
- moveDictExtensions( art, extensionsMap,
179
- (art.enum && art.kind !== 'annotate' ? 'enum' : 'elements'), 'elements' );
180
+ let elementsProp = 'elements';
181
+ if (art.kind !== 'annotate')
182
+ elementsProp = art.enum && 'enum' || art.foreignKeys && 'foreignKeys' || 'elements';
183
+ moveDictExtensions( art, extensionsMap, elementsProp, 'elements' );
180
184
  moveDictExtensions( art, extensionsMap, 'enum' );
181
185
  }
182
186
  }
183
187
 
188
+ /**
189
+ * Applying extensions is handled in extendArtifactAfter(). And only afterward,
190
+ * an effective sequence number is set. Meaning that if a sub-artifact already
191
+ * has a sequence number, then extensions would be lost.
192
+ */
193
+ function ensureArtifactNotProcessed( art ) {
194
+ if (!model.options.testMode)
195
+ return;
196
+
197
+ if (art.$effectiveSeqNo !== 0 && art.$effectiveSeqNo !== undefined) {
198
+ // if the artifact already has a sequence number, then
199
+ // extendArtifactAfter() was already called -> annotations would be lost.
200
+ throw new CompilerAssertion('artifact already processed; extensions would be lost');
201
+ }
202
+ }
203
+
184
204
  /**
185
205
  * Create super annotate statements for remaining extensions
186
206
  */
@@ -196,15 +216,25 @@ function extend( model ) {
196
216
  }
197
217
 
198
218
  // TODO: delete again - if not, what about extensions in contexts/services?
219
+ // Check test.lsp-api.js! Links in extensions are needed.
199
220
  function setArtifactLinkForExtensions( source ) {
200
221
  if (!source.extensions)
201
222
  return;
202
- for (const ext of source.extensions ) {
223
+ for (const ext of source.extensions) {
224
+ if (!ext.name?.id)
225
+ continue;
226
+
203
227
  const { name } = ext;
204
- if (name?.id && name._artifact === undefined) {
228
+ const { path } = name;
229
+ if (name._artifact === undefined) {
205
230
  const refCtx = (name.id.startsWith( 'localized.' )) ? '_extensions' : ext.kind;
206
231
  resolvePath( name, refCtx, ext ); // for LSP
207
232
  }
233
+ else if (model.options.lspMode && path?.[0]._artifact === undefined) {
234
+ // we don't use resolvePath(…,'extend'), as that would add a dependency
235
+ resolveDefinitionName( ext );
236
+ setArtifactLink( path[path.length - 1], name._artifact );
237
+ }
208
238
  }
209
239
  }
210
240
 
@@ -224,7 +254,7 @@ function extend( model ) {
224
254
  code: 'extend … with definitions',
225
255
  keyword: 'extend service',
226
256
  };
227
- // TODO(v5): Discuss: make this an error?
257
+ // TODO(v6): Discuss: make this an error?
228
258
  warning( 'ext-invalid-kind', [ loc, ext ], msgArgs, {
229
259
  std: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) instead',
230
260
  annotate: 'There is no artifact $(ART), use $(CODE) instead',
@@ -393,6 +423,9 @@ function extend( model ) {
393
423
  for (const col of ext.columns)
394
424
  col.$extended = true;
395
425
 
426
+ if (art.kind === 'annotate' && art.$inferred === '')
427
+ return; // internal super-annotate for unknown artifacts
428
+
396
429
  if (!query?.from?.path) {
397
430
  const variant = (query?.from || query)?.op?.val || 'std';
398
431
  error( 'extend-columns', [ ext.columns[$location], ext ], { '#': variant, art } );
@@ -490,7 +523,7 @@ function extend( model ) {
490
523
  if ('val' in upToSpec) {
491
524
  if (previousItem.val === upToSpec.val) // enum, struct and ref have no val
492
525
  return true;
493
- // TODO v5: delete the speciao UP TO comparison?
526
+ // TODO v6: delete the special UP TO comparison?
494
527
  const upToVal = upToSpec.val;
495
528
  const prevVal = previousItem.val;
496
529
  // eslint-disable-next-line eqeqeq
@@ -540,7 +573,7 @@ function extend( model ) {
540
573
  // - 'ext-unexpected-type-argument' (TODO) if the artifact does not have the prop
541
574
  // - 'ext-invalid-type-argument' if the value is wrong for extend (no overwrite)
542
575
  //
543
- // TODO v5: do not allow `extend … with (precision: …)` alone if original def also has `scale`
576
+ // TODO v6: do not allow `extend … with (precision: …)` alone if original def also has `scale`
544
577
  function applyTypeExtensions( art, ext, prop, scaleDiff ) {
545
578
  // console.log('ATE:',art?.[prop],ext?.[prop],scaleDiff)
546
579
  if (!ext?.[prop])
@@ -617,17 +650,17 @@ function extend( model ) {
617
650
  const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate
618
651
 
619
652
  for (const ext of extensions) {
620
- const extDict = ext[extProp];
621
653
  let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement
622
- for (const name in extDict) {
623
- const elemExt = extDict[name];
654
+ forEachGeneric(ext, extProp, (elemExt, name) => {
624
655
  if (elemExt.kind !== 'annotate' && elemExt.kind !== 'extend') // TODO: specified elems
625
- continue; // definitions inside extend, already handled
656
+ return; // definitions inside extend, already handled
626
657
  dictCheck = dictCheck && checkRemainingMemberExtensions( art, elemExt, artProp, name );
627
658
  const elem = artDict[name] || annotateFor( art, extProp, name );
628
659
  setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null ) );
629
- pushToDict( elem, '_extensions', elemExt );
630
- }
660
+ ensureArtifactNotProcessed( elem );
661
+ if (elem.$duplicates !== true)
662
+ pushToDict( elem, '_extensions', elemExt );
663
+ });
631
664
  }
632
665
  }
633
666
 
@@ -778,6 +811,10 @@ function extend( model ) {
778
811
  { '#': (inReturns ? 'enum-returns' : 'enum'), art, name },
779
812
  parent.enum );
780
813
  break;
814
+ case 'foreignKeys':
815
+ notFound( 'ext-undefined-key', ext.name.location, ext,
816
+ { name }, parent.foreignKeys );
817
+ break;
781
818
  case 'params':
782
819
  notFound( 'ext-undefined-param', ext.name.location, ext,
783
820
  { '#': 'param', art: parent, name },
@@ -791,7 +828,8 @@ function extend( model ) {
791
828
  parent.actions );
792
829
  break;
793
830
  default:
794
- // assert
831
+ if (model.options.testMode)
832
+ throw new CompilerAssertion(`Missing case for prop: ${ prop }`);
795
833
  }
796
834
  }
797
835
  return true;
@@ -832,8 +870,9 @@ function extend( model ) {
832
870
  }
833
871
 
834
872
  function checkRemainingMainExtensions( art, ext, localized ) {
873
+ const isExtend = ext.kind === 'extend';
835
874
  if (localized) {
836
- if (isV5preview && ext.kind === 'extend') {
875
+ if (isExtend) {
837
876
  // In v5, reject any `extend` on localized.
838
877
  error( 'ref-undefined-art', [ ext.location || ext.name.location, ext ],
839
878
  { '#': 'localized', keyword: 'annotate' } );
@@ -847,22 +886,21 @@ function extend( model ) {
847
886
  if (art?.builtin) {
848
887
  info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location?
849
888
  }
850
- else if (art?.kind === 'namespace') {
889
+ else if (isExtend && art?.kind === 'namespace') {
890
+ // `annotate` on namespaces already handled before
851
891
  const hasAnnotations = Object.keys(ext).find(a => a.charAt(0) === '@');
852
892
  const firstAnno = ext[hasAnnotations];
853
893
  // In v5, extending namespaces is only allowed for `extend with definitions`.
854
894
  // Neither annotations nor other extensions are allowed.
855
895
  // Non-artifact extensions are reported in resolvePath() already (for v5).
856
- if ((hasAnnotations || !ext.artifacts) ) {
857
- if (isV5preview) {
858
- error( 'ref-undefined-art', [ (firstAnno?.name || ext.name).location, ext ], {
859
- '#': 'namespace', art: ext,
860
- } );
861
- }
862
- else {
863
- info( 'anno-namespace', [ (firstAnno?.name || ext.name).location, ext ], {},
864
- 'Namespaces can\'t be annotated nor extended' );
865
- }
896
+ // Because "namespaces" are the same as "unknown" artifacts in CSN, we don't report
897
+ // an error for `annotate`s.
898
+ // FIXME: The compiler generates empty `annotate` statements for
899
+ // `extend ns with definitions {…}`. That's why we check the frontend.
900
+ if (hasAnnotations || (!ext.artifacts && ext._block.$frontend !== 'json')) {
901
+ error( 'ref-undefined-art', [ (firstAnno?.name || ext.name).location, ext ], {
902
+ '#': 'namespace', art: ext,
903
+ } );
866
904
  }
867
905
  }
868
906
  }
@@ -1101,7 +1139,6 @@ function extend( model ) {
1101
1139
  { art: artName },
1102
1140
  {
1103
1141
  std: 'Unknown $(ART) - nothing to extend',
1104
- // eslint-disable-next-line max-len
1105
1142
  element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend',
1106
1143
  action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
1107
1144
  } );
@@ -1232,7 +1269,7 @@ function extend( model ) {
1232
1269
  if (!parent) // not yet set for EXTEND foo WITH bar => linkToOrigin() did not add it
1233
1270
  dictAdd( ext[prop], name, elem );
1234
1271
  elem.$inferred = 'include';
1235
- if (origin.masked) // TODO(v5): remove 'masked'
1272
+ if (origin.masked) // TODO(v6): remove 'masked'
1236
1273
  elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );
1237
1274
  if (origin.key)
1238
1275
  elem.key = Object.assign( { $inferred: 'include' }, origin.key );
@@ -1269,8 +1306,6 @@ function extend( model ) {
1269
1306
  /**
1270
1307
  * Report duplicates in parent[prop] that happen due to multiple includes having the
1271
1308
  * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
1272
- *
1273
- * TODO(v5): Make this a hard error; see checkRedefinition(); maybe combine both;
1274
1309
  */
1275
1310
  function checkRedefinitionThroughIncludes( parent, prop ) {
1276
1311
  if (!parent[prop])
@@ -656,7 +656,7 @@ function generate( model ) {
656
656
 
657
657
  if (elem.type && !isDirectComposition( elem )) {
658
658
  // Only issue warning for direct usages, not for projections, includes, etc.
659
- // TODO: Make it configurable error; v5: error
659
+ // TODO: Make it configurable error; v6: error
660
660
  // TODO: move to resolve.js where we test the targetAspect,
661
661
  warning( 'type-expecting-composition', [ elem.type.location, elem ],
662
662
  { newcode: 'Composition of', code: 'Association to' },
@@ -83,11 +83,7 @@ function parseX( source, filename, options = {}, messageFunctions = null ) {
83
83
  if (!messageFunctions)
84
84
  messageFunctions = createMessageFunctions( options, 'parse' );
85
85
  const ext = path.extname( filename ).slice(1).toLowerCase();
86
- // eslint-disable-next-line no-nested-ternary
87
- const parser = options.fallbackParser === 'auto!'
88
- ? (source?.startsWith( '{' ) ? parseCsn.parse : parseLanguage)
89
- : (extensionParsers[ext] || extensionParsers[options.fallbackParser] ||
90
- source.startsWith( '{' ) && parseCsn.parse);
86
+ const parser = parserForFile( source, ext, options );
91
87
  if (parser)
92
88
  return parser( source, filename, options, messageFunctions );
93
89
 
@@ -101,6 +97,27 @@ function parseX( source, filename, options = {}, messageFunctions = null ) {
101
97
  return model;
102
98
  }
103
99
 
100
+ /**
101
+ * Get the correct parser for the given source / file extension.
102
+ * Respects the set fallback parser.
103
+ *
104
+ * @param {string} source
105
+ * @param {string} ext
106
+ * @param {object} options
107
+ */
108
+ function parserForFile( source, ext, options ) {
109
+ // 'auto!' ignores the file's extension
110
+ if (options.fallbackParser === 'auto!')
111
+ return (source?.startsWith( '{' ) ? parseCsn.parse : parseLanguage);
112
+
113
+ if (options.fallbackParser === 'csn!')
114
+ return parseCsn.parse;
115
+
116
+ return extensionParsers[ext] ||
117
+ extensionParsers[options.fallbackParser] ||
118
+ (source.startsWith( '{' ) && parseCsn.parse);
119
+ }
120
+
104
121
  // Main function: Compile the sources from the files given by the array of
105
122
  // `filenames`. As usual with the `fs` library, relative file names are
106
123
  // relative to the working directory `process.cwd()`. With argument `dir`, the
@@ -530,7 +547,7 @@ function processFilenamesSync( filenames, dir ) {
530
547
  // already handles non-existent files.
531
548
  name = fs.realpathSync.native( name );
532
549
  }
533
- catch (e) {
550
+ catch {
534
551
  // Ignore the not-found (ENOENT) error
535
552
  }
536
553
  filenameMap[originalName] = name;