@sap/cds-compiler 3.0.0 → 3.1.2

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 (79) hide show
  1. package/CHANGELOG.md +104 -9
  2. package/bin/.eslintrc.json +2 -1
  3. package/bin/cdsc.js +28 -16
  4. package/doc/API.md +11 -0
  5. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  6. package/doc/CHANGELOG_BETA.md +24 -2
  7. package/doc/CHANGELOG_DEPRECATED.md +21 -1
  8. package/lib/api/main.js +92 -40
  9. package/lib/api/options.js +2 -3
  10. package/lib/base/keywords.js +64 -1
  11. package/lib/base/message-registry.js +33 -5
  12. package/lib/base/messages.js +54 -65
  13. package/lib/base/model.js +2 -0
  14. package/lib/base/optionProcessorHelper.js +53 -21
  15. package/lib/checks/actionsFunctions.js +8 -7
  16. package/lib/checks/selectItems.js +96 -14
  17. package/lib/checks/types.js +5 -8
  18. package/lib/checks/validator.js +1 -2
  19. package/lib/compiler/assert-consistency.js +65 -13
  20. package/lib/compiler/base.js +6 -4
  21. package/lib/compiler/builtins.js +93 -4
  22. package/lib/compiler/checks.js +1 -1
  23. package/lib/compiler/define.js +28 -23
  24. package/lib/compiler/extend.js +20 -11
  25. package/lib/compiler/finalize-parse-cdl.js +5 -9
  26. package/lib/compiler/index.js +2 -0
  27. package/lib/compiler/populate.js +37 -32
  28. package/lib/compiler/propagator.js +11 -6
  29. package/lib/compiler/resolve.js +15 -19
  30. package/lib/compiler/shared.js +54 -18
  31. package/lib/compiler/tweak-assocs.js +5 -11
  32. package/lib/compiler/utils.js +15 -6
  33. package/lib/edm/annotations/genericTranslation.js +12 -2
  34. package/lib/edm/annotations/preprocessAnnotations.js +18 -15
  35. package/lib/edm/csn2edm.js +18 -17
  36. package/lib/edm/edm.js +22 -13
  37. package/lib/edm/edmAnnoPreprocessor.js +349 -0
  38. package/lib/edm/edmInboundChecks.js +85 -0
  39. package/lib/edm/edmPreprocessor.js +336 -665
  40. package/lib/edm/edmUtils.js +86 -45
  41. package/lib/gen/Dictionary.json +29 -9
  42. package/lib/gen/language.checksum +1 -1
  43. package/lib/gen/language.interp +1 -2
  44. package/lib/gen/languageLexer.js +3 -0
  45. package/lib/gen/languageParser.js +4332 -4496
  46. package/lib/inspect/.eslintrc.json +4 -0
  47. package/lib/inspect/index.js +14 -0
  48. package/lib/inspect/inspectModelStatistics.js +81 -0
  49. package/lib/inspect/inspectPropagation.js +189 -0
  50. package/lib/inspect/inspectUtils.js +44 -0
  51. package/lib/json/from-csn.js +19 -20
  52. package/lib/json/to-csn.js +11 -8
  53. package/lib/language/genericAntlrParser.js +150 -92
  54. package/lib/language/language.g4 +47 -74
  55. package/lib/main.d.ts +1 -0
  56. package/lib/model/api.js +1 -1
  57. package/lib/model/csnRefs.js +56 -29
  58. package/lib/model/csnUtils.js +29 -14
  59. package/lib/model/revealInternalProperties.js +6 -4
  60. package/lib/modelCompare/compare.js +3 -0
  61. package/lib/optionProcessor.js +81 -38
  62. package/lib/render/toCdl.js +57 -32
  63. package/lib/render/toHdbcds.js +1 -1
  64. package/lib/render/toSql.js +31 -11
  65. package/lib/render/utils/common.js +3 -4
  66. package/lib/transform/db/associations.js +43 -35
  67. package/lib/transform/db/cdsPersistence.js +0 -1
  68. package/lib/transform/db/flattening.js +3 -4
  69. package/lib/transform/db/transformExists.js +7 -5
  70. package/lib/transform/draft/db.js +1 -1
  71. package/lib/transform/forHanaNew.js +11 -2
  72. package/lib/transform/forOdataNew.js +4 -4
  73. package/lib/transform/localized.js +15 -11
  74. package/lib/transform/odata/typesExposure.js +14 -5
  75. package/lib/utils/file.js +28 -18
  76. package/lib/utils/moduleResolve.js +0 -1
  77. package/package.json +3 -4
  78. package/share/messages/syntax-expected-integer.md +9 -8
  79. package/lib/checks/unknownMagic.js +0 -41
@@ -12,7 +12,8 @@ const { dictAdd, dictAddArray } = require('../base/dictionaries');
12
12
  const locUtils = require('../base/location');
13
13
  const { parseDocComment } = require('./docCommentParser');
14
14
  const { parseMultiLineStringLiteral } = require('./multiLineStringParser');
15
- const { functionsWithoutParens, specialFunctions } = require('../compiler/builtins');
15
+ const { functionsWithoutParens, specialFunctions, quotedLiteralPatterns } = require('../compiler/builtins');
16
+ const { pathName } = require("../compiler/utils");
16
17
 
17
18
  const $location = Symbol.for('cds.$location');
18
19
 
@@ -62,8 +63,13 @@ Object.assign(GenericAntlrParser.prototype, {
62
63
  info: function(...args) { return _message( this, 'info', ...args ); },
63
64
  attachLocation,
64
65
  assignAnnotation,
66
+ addAnnotation,
67
+ checkExtensionDict,
68
+ handleDuplicateExtension,
65
69
  startLocation,
66
70
  tokenLocation,
71
+ isMultiLineToken,
72
+ fixMultiLineTokenEndLocation,
67
73
  valueWithTokenLocation,
68
74
  previousTokenAtLocation,
69
75
  combinedLocation,
@@ -113,40 +119,6 @@ Object.assign(GenericAntlrParser.prototype, {
113
119
  parseMultiLineStringLiteral,
114
120
  });
115
121
 
116
- // Patterns for literal token tests and creation. The value is a map from the
117
- // `prefix` argument of function `quotedliteral` to the following properties:
118
- // - `test_msg`: error message which is issued if `test_fn` or `test_re` fail.
119
- // - `test_fn`: function called with argument `value`, fails falsy return value
120
- // - `test_re`: regular expression, fails if it does not match argument `value`
121
- // - `unexpected_msg`: error message which is issued if `unexpected_char` matches
122
- // - `unexpected_char`: regular expression matching an illegal character in `value`,
123
- // the error location is only correct for a literal <prefix>'<value>'
124
- // - `literal`: the value which is used instead of `prefix` in the AST
125
- // TODO: we might do a range check (consider leap seconds, i.e. max value 60),
126
- // but always allow Feb 29 (no leap year computation)
127
- // TODO: make it a configurable error (syntax-invalid-literal)
128
- // TODO: also use for CSN input
129
- const quotedLiteralPatterns = {
130
- x: {
131
- test_variant: 'uneven-hex',
132
- test_fn: (str => Number.isInteger(str.length / 2)),
133
- unexpected_variant: 'invalid-hex',
134
- unexpected_char: /[^0-9a-f]/i,
135
- },
136
- time: {
137
- test_variant: 'time',
138
- test_re: /^[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?$/,
139
- },
140
- date: {
141
- test_variant: 'date',
142
- test_re: /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/,
143
- },
144
- timestamp: {
145
- test_variant: 'timestamp',
146
- test_re: /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]{1,7})?)?$/,
147
- },
148
- };
149
-
150
122
  // Use the following function for language constructs which we (currently)
151
123
  // just being able to parse, in able to run tests from HANA CDS. As soon as we
152
124
  // create ASTs for the language construct and put it into a CSN, a
@@ -221,12 +193,12 @@ function setLocalTokenIfBefore( string, tokenName, before, inSameLine ) {
221
193
  }
222
194
 
223
195
  function setLocalTokenForId( tokenNameMap ) {
196
+ const tokenName = tokenNameMap[ this._input.LT(2).text || '' ];
224
197
  const ll1 = this.getCurrentToken();
225
- if (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )) {
226
- const tokenName = tokenNameMap[ this._input.LT(2).text || '' ];
227
- if (tokenName)
228
- ll1.type = this.constructor[tokenName];
229
- }
198
+ if (tokenName &&
199
+ (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )))
200
+ ll1.type = this.constructor[tokenName];
201
+ return !!tokenName;
230
202
  }
231
203
 
232
204
  // // Special function for rule `requiredSemi` before return $ctx
@@ -298,9 +270,6 @@ function prepareGenericKeywords( pathItem, expected = null) {
298
270
  const func = pathItem?.id && specialFunctions[pathItem.id.toUpperCase()];
299
271
  const spec = func && func[argPos] || specialFunctions[''][argPos ? 1 : 0];
300
272
  this.$genericKeywords = spec;
301
- // currently, we only have 'TODO', i.e. a keyword which is alternative to expression
302
- // TODO: If not just at the beginning, we need a stack for $genericKeywords,
303
- // as we can have nested special functions
304
273
  // @ts-ignore
305
274
  const token = this.getCurrentToken() || { text: '' };
306
275
  const text = token.text.toUpperCase();
@@ -339,15 +308,26 @@ function reportErrorForGenericKeyword() {
339
308
  function attachLocation( art ) {
340
309
  if (!art || art.$parens)
341
310
  return art;
342
- if (!art.location)
343
- art.location = this.startLocation();
344
- const { stop } = this._ctx;
345
- art.location.endLine = stop.line;
346
- art.location.endCol = stop.stop - stop.start + stop.column + 2; // after the last char (special for EOF?)
311
+ if (!art.location) {
312
+ art.location = this.tokenLocation(this._ctx.start, this._ctx.stop);
313
+ return art;
314
+ }
315
+
316
+ // The last token (this._ctx.stop) may be a multi-line string literal, in which
317
+ // case we can't rely on `this._ctx.stop.line`.
318
+ if (this.isMultiLineToken(this._ctx.stop)) {
319
+ this.fixMultiLineTokenEndLocation(this._ctx.stop, art.location);
320
+
321
+ } else {
322
+ const { stop } = this._ctx;
323
+ art.location.endLine = stop.line;
324
+ art.location.endCol = stop.stop - stop.start + stop.column + 2; // after the last char (special for EOF?)
325
+ }
326
+
347
327
  return art;
348
328
  }
349
329
 
350
- function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
330
+ function assignAnnotation( art, anno, prefix = '', iHaveVariant = false ) {
351
331
  const { name, $flatten } = anno;
352
332
  const { path } = name;
353
333
  if (path.broken || !path[path.length - 1].id)
@@ -355,7 +335,8 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
355
335
  const pathname = pathName( path );
356
336
  let absolute = '';
357
337
  if (name.variant) {
358
- absolute = `${ prefix }${ pathname }#${ name.variant.id }`;
338
+ const variant = pathName( name.variant.path );
339
+ absolute = `${ prefix }${ pathname }#${ variant }`;
359
340
  if (iHaveVariant) { // TODO: do we really care in the parser / core compiler?
360
341
  this.error( 'anno-duplicate-variant', [ name.variant.location ],
361
342
  {}, // TODO: params
@@ -374,18 +355,9 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
374
355
  }
375
356
  else {
376
357
  name.absolute = absolute;
377
- const prop = '@' + absolute;
378
- const old = art[prop];
379
- if (old && old.$inferred)
380
- art[prop] = anno;
381
- else
382
- dictAddArray( art, prop, anno, (n, location, a) => {
383
- this.error( 'syntax-duplicate-anno', [ location ], { anno: n },
384
- 'Duplicate assignment with $(ANNO)' );
385
- a.$errorReported = true; // do not report again later as anno-duplicate-xyz
386
- } );
358
+ this.addAnnotation( art, '@' + absolute, anno );
387
359
  }
388
- if (!prefix) { // set deprecated $annnotations for cds-lsp
360
+ if (!prefix) { // set deprecated $annotations for cds-lsp
389
361
  if (!art.$annotations)
390
362
  art.$annotations = [];
391
363
  const location = locUtils.combinedLocation( anno.name, anno );
@@ -393,6 +365,76 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
393
365
  }
394
366
  }
395
367
 
368
+ function addAnnotation( art, prop, anno ) {
369
+ dictAddArray( art, prop, anno, (n, location, a) => {
370
+ // if we would make it a warning, we would still need to keep it an error
371
+ // with '...'; otherwise parse.cdl would have to split annotate statements
372
+ this.error( 'syntax-duplicate-anno', [ location ], { anno: n },
373
+ 'Duplicate assignment with $(ANNO)' );
374
+ a.$errorReported = 'syntax-duplicate-anno';
375
+ // do not report again later as anno-duplicate-xyz
376
+ } );
377
+ }
378
+
379
+ const extensionDicts = { elements: true, enum: true, params: true, returns: true };
380
+
381
+ function checkExtensionDict( dict ) {
382
+ for (const name in dict) {
383
+ const def = dict[name];
384
+ if (!def.$duplicates)
385
+ continue;
386
+
387
+ if (def.kind !== 'annotate') {
388
+ const numDefines =
389
+ def.$duplicates.reduce( addOneForDefinition, addOneForDefinition( 0, def ) );
390
+ this.handleDuplicateExtension( def, name, numDefines );
391
+ for (const dup of def.$duplicates)
392
+ this.handleDuplicateExtension( dup, name, numDefines );
393
+ continue;
394
+ }
395
+ // move annotations, 'doc' and 'elements' etc to main member
396
+ for (const dup of def.$duplicates) {
397
+ for (const prop of Object.keys( dup )) {
398
+ if (prop.charAt(0) === '@') {
399
+ this.addAnnotation( def, prop, dup[prop] )
400
+ }
401
+ else if (prop === 'doc') {
402
+ if (def.doc)
403
+ this.warning( 'syntax-duplicate-doc-comment', def.doc.location, {},
404
+ 'Doc comment is overwritten by another one below' );
405
+ def.doc = dup.doc;
406
+ }
407
+ else if (extensionDicts[prop]) {
408
+ if (def[prop])
409
+ this.message( 'syntax-duplicate-annotate', [ def.name.location ], { name, prop } );
410
+ def[prop] = dup[prop]; // continuation semantics: last wins
411
+ }
412
+ }
413
+ }
414
+ def.$duplicates = null;
415
+ }
416
+ }
417
+
418
+ function addOneForDefinition( count, ext ) {
419
+ return (ext.kind === 'extend') ? count : count + 1;
420
+ }
421
+
422
+ /**
423
+ * Handle duplicate extensions. Does not handle `annotate`.
424
+ *
425
+ * @param {XSN.Extension} ext
426
+ * @param {string} name
427
+ * @param {number} numDefines
428
+ */
429
+ function handleDuplicateExtension( ext, name, numDefines ) {
430
+ if (ext.kind === 'extend')
431
+ this.error( 'syntax-duplicate-extend', [ ext.name.location ],
432
+ { name, '#': (numDefines ? 'define' : 'extend') } );
433
+ else if (numDefines === 1)
434
+ ext.$errorReported = 'syntax-duplicate-extend'; // a definition, but not duplicate
435
+ }
436
+
437
+
396
438
  /**
397
439
  * Return start location of `token`, or the first token matched by the current
398
440
  * rule if `token` is undefined
@@ -433,31 +475,49 @@ function tokenLocation( token, endToken = null ) {
433
475
 
434
476
  // This check is done for performance reason. No need to access a token's
435
477
  // data if we know that it spans only one single line.
436
- const isMultiLineToken = (
437
- endToken.type === this.constructor.DocComment ||
438
- endToken.type === this.constructor.String ||
439
- endToken.type === this.constructor.UnterminatedLiteral
478
+ if (this.isMultiLineToken(token))
479
+ this.fixMultiLineTokenEndLocation(token, loc);
480
+
481
+ return loc;
482
+ }
483
+
484
+ function isMultiLineToken(token) {
485
+ return (
486
+ token.type === this.constructor.DocComment ||
487
+ token.type === this.constructor.String ||
488
+ token.type === this.constructor.UnterminatedLiteral
440
489
  );
441
- if (isMultiLineToken) {
442
- // Count the number of newlines in the token.
443
- const source = endToken.source[1].data;
444
- let newLineCount = 0;
445
- let lastNewlineIndex = endToken.start;
446
- for (let i = endToken.start; i < endToken.stop; i++) {
447
- // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
448
- // because ANTLR only uses LF for line break detection.
449
- if (source[i] === 10) { // code point of '\n'
450
- newLineCount++;
451
- lastNewlineIndex = i;
452
- }
453
- }
454
- if (newLineCount > 0) {
455
- loc.endLine = endToken.line + newLineCount;
456
- loc.endCol = endToken.stop - lastNewlineIndex + 1;
490
+ }
491
+
492
+ /**
493
+ * Adapt end location of `location` according to `token`, assuming that `token` is a multi-line
494
+ * token such as a multi-line string or doc comment.
495
+ *
496
+ * Sets `endLine`/`endCol`, respecting newline characters in the token.
497
+ *
498
+ * @param token
499
+ * @param {CSN.Location} location
500
+ */
501
+ function fixMultiLineTokenEndLocation( token, location ) {
502
+ // Count the number of newlines in the token.
503
+ const source = token.source[1].data;
504
+ let newLineCount = 0;
505
+ let lastNewlineIndex = token.start;
506
+ for (let i = token.start; i < token.stop; i++) {
507
+ // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
508
+ // because ANTLR only uses LF for line break detection.
509
+ if (source[i] === 10) { // code point of '\n'
510
+ newLineCount++;
511
+ lastNewlineIndex = i;
457
512
  }
458
513
  }
459
-
460
- return loc;
514
+ if (newLineCount > 0) {
515
+ location.endLine = token.line + newLineCount;
516
+ location.endCol = token.stop - lastNewlineIndex + 1;
517
+ } else {
518
+ location.endLine = token.line;
519
+ location.endCol = token.stop - token.start + token.column + 2; // after the last char (special for EOF?)
520
+ }
461
521
  }
462
522
 
463
523
  /**
@@ -504,7 +564,7 @@ function surroundByParens( expr, open, close, asQuery = false ) {
504
564
  }
505
565
 
506
566
  function unaryOpForParens( query, val ) {
507
- const parens = query.$parens;
567
+ const parens = query?.$parens;
508
568
  if (!parens)
509
569
  return query;
510
570
  const location = parens[parens.length - 1];
@@ -530,8 +590,8 @@ function docComment( node ) {
530
590
  if (!this.options.docComment)
531
591
  return;
532
592
  if (node.doc) {
533
- this.warning( 'syntax-duplicate-doc-comment', token, {},
534
- 'Repeated doc comment - previous doc is replaced' );
593
+ this.warning( 'syntax-duplicate-doc-comment', node.doc.location, {},
594
+ 'Doc comment is overwritten by another one below' );
535
595
  }
536
596
  node.doc = this.valueWithTokenLocation( parseDocComment( token.text ), token );
537
597
  }
@@ -715,8 +775,7 @@ function quotedLiteral( token, literal ) {
715
775
  literal = token.text.slice( 0, pos - 1 ).toLowerCase();
716
776
  const p = quotedLiteralPatterns[literal] || {};
717
777
 
718
- if ((p.test_fn && !p.test_fn(val) || p.test_re && !p.test_re.test(val)) &&
719
- !this.options.parseOnly)
778
+ if (p.test_fn && !p.test_fn(val) && !this.options.parseOnly)
720
779
  this.warning( 'syntax-invalid-literal', location, { '#': p.test_variant } );
721
780
 
722
781
  if (p.unexpected_char) {
@@ -743,10 +802,6 @@ function quotedLiteral( token, literal ) {
743
802
  }
744
803
  }
745
804
 
746
- function pathName( path, brokenName ) {
747
- return (path && !path.broken) ? path.map( id => id.id ).join('.') : brokenName;
748
- }
749
-
750
805
  function pushIdent( path, ident, prefix ) {
751
806
  if (!ident) {
752
807
  path.broken = true;
@@ -981,6 +1036,9 @@ function handleComposition( cardinality, isComposition ) {
981
1036
  }
982
1037
 
983
1038
  function associationInSelectItem( art ) {
1039
+ if (!art.value) // e.g. `expand` without value (for new structures)
1040
+ return;
1041
+
984
1042
  const isPath = art.value.path && art.value.path.length
985
1043
  const isIdentifier = isPath && art.value.path.length === 1;
986
1044
  if (isIdentifier) {