@sap/cds-compiler 2.10.4 → 2.12.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 (103) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +10 -8
  4. package/bin/cdsc.js +58 -35
  5. package/bin/cdsse.js +1 -0
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  8. package/doc/CHANGELOG_BETA.md +16 -0
  9. package/lib/api/.eslintrc.json +2 -0
  10. package/lib/api/main.js +10 -36
  11. package/lib/api/options.js +17 -8
  12. package/lib/api/validate.js +30 -3
  13. package/lib/backends.js +12 -13
  14. package/lib/base/dictionaries.js +2 -1
  15. package/lib/base/keywords.js +3 -2
  16. package/lib/base/message-registry.js +64 -11
  17. package/lib/base/messages.js +38 -18
  18. package/lib/base/model.js +6 -4
  19. package/lib/base/optionProcessorHelper.js +148 -86
  20. package/lib/checks/.eslintrc.json +2 -0
  21. package/lib/checks/actionsFunctions.js +2 -1
  22. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  23. package/lib/checks/foreignKeys.js +4 -4
  24. package/lib/checks/managedInType.js +4 -4
  25. package/lib/checks/queryNoDbArtifacts.js +1 -3
  26. package/lib/checks/selectItems.js +4 -0
  27. package/lib/checks/sql-snippets.js +93 -0
  28. package/lib/checks/unknownMagic.js +6 -3
  29. package/lib/checks/validator.js +8 -0
  30. package/lib/compiler/assert-consistency.js +14 -5
  31. package/lib/compiler/base.js +64 -0
  32. package/lib/compiler/builtins.js +62 -16
  33. package/lib/compiler/checks.js +34 -10
  34. package/lib/compiler/definer.js +91 -112
  35. package/lib/compiler/index.js +30 -30
  36. package/lib/compiler/propagator.js +8 -4
  37. package/lib/compiler/resolver.js +279 -63
  38. package/lib/compiler/shared.js +65 -230
  39. package/lib/compiler/utils.js +191 -0
  40. package/lib/edm/annotations/genericTranslation.js +35 -18
  41. package/lib/edm/annotations/preprocessAnnotations.js +1 -1
  42. package/lib/edm/csn2edm.js +4 -3
  43. package/lib/edm/edm.js +8 -8
  44. package/lib/edm/edmPreprocessor.js +61 -59
  45. package/lib/edm/edmUtils.js +14 -15
  46. package/lib/gen/Dictionary.json +82 -40
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +19 -1
  49. package/lib/gen/language.tokens +80 -73
  50. package/lib/gen/languageLexer.interp +27 -1
  51. package/lib/gen/languageLexer.js +925 -826
  52. package/lib/gen/languageLexer.tokens +72 -65
  53. package/lib/gen/languageParser.js +4817 -4102
  54. package/lib/json/from-csn.js +57 -26
  55. package/lib/json/to-csn.js +244 -51
  56. package/lib/language/antlrParser.js +12 -1
  57. package/lib/language/docCommentParser.js +1 -1
  58. package/lib/language/errorStrategy.js +26 -8
  59. package/lib/language/genericAntlrParser.js +106 -30
  60. package/lib/language/language.g4 +200 -70
  61. package/lib/language/multiLineStringParser.js +536 -0
  62. package/lib/main.d.ts +220 -21
  63. package/lib/main.js +6 -3
  64. package/lib/model/api.js +2 -2
  65. package/lib/model/csnRefs.js +218 -86
  66. package/lib/model/csnUtils.js +99 -178
  67. package/lib/model/enrichCsn.js +84 -43
  68. package/lib/model/revealInternalProperties.js +25 -8
  69. package/lib/model/sortViews.js +8 -1
  70. package/lib/modelCompare/compare.js +2 -1
  71. package/lib/optionProcessor.js +33 -18
  72. package/lib/render/.eslintrc.json +1 -2
  73. package/lib/render/DuplicateChecker.js +2 -2
  74. package/lib/render/manageConstraints.js +1 -1
  75. package/lib/render/toCdl.js +202 -82
  76. package/lib/render/toHdbcds.js +194 -135
  77. package/lib/render/toRename.js +7 -10
  78. package/lib/render/toSql.js +91 -51
  79. package/lib/render/utils/common.js +24 -5
  80. package/lib/render/utils/sql.js +6 -4
  81. package/lib/transform/braceExpression.js +4 -2
  82. package/lib/transform/db/applyTransformations.js +189 -0
  83. package/lib/transform/db/associations.js +389 -0
  84. package/lib/transform/db/cdsPersistence.js +150 -0
  85. package/lib/transform/db/constraints.js +275 -119
  86. package/lib/transform/db/draft.js +6 -4
  87. package/lib/transform/db/expansion.js +10 -9
  88. package/lib/transform/db/flattening.js +23 -8
  89. package/lib/transform/db/temporal.js +236 -0
  90. package/lib/transform/db/transformExists.js +106 -25
  91. package/lib/transform/db/views.js +485 -0
  92. package/lib/transform/forHanaNew.js +90 -1036
  93. package/lib/transform/forOdataNew.js +11 -3
  94. package/lib/transform/localized.js +5 -14
  95. package/lib/transform/odata/generateForeignKeyElements.js +2 -2
  96. package/lib/transform/transformUtilsNew.js +34 -20
  97. package/lib/transform/translateAssocsToJoins.js +15 -23
  98. package/lib/transform/universalCsnEnricher.js +217 -47
  99. package/lib/utils/file.js +13 -6
  100. package/lib/utils/term.js +65 -42
  101. package/lib/utils/timetrace.js +55 -27
  102. package/package.json +1 -1
  103. package/lib/transform/db/helpers.js +0 -58
@@ -114,9 +114,10 @@ function sync( recognizer ) {
114
114
  }
115
115
  return;
116
116
  }
117
- // TODO: expected token is identifier, current is KEYWORD
118
117
 
119
118
  if (nextTokens.contains(antlr4.Token.EPSILON)) {
119
+ // when exiting a (innermost) rule, remember the state to make
120
+ // getExpectedTokensForMessage() calculate the full "expected set"
120
121
  if (recognizer.$nextTokensToken !== token) {
121
122
  // console.log('SET:',token.type,recognizer.state,recognizer.$nextTokensToken && recognizer.$nextTokensToken.type)
122
123
  recognizer.$nextTokensToken = token;
@@ -126,13 +127,29 @@ function sync( recognizer ) {
126
127
  return;
127
128
  }
128
129
 
130
+ // Expected token is identifier, current is (reserved) KEYWORD:
131
+ // TODO: do not use this if "close enough" (1 char diff) to a keyword in nextTokens
132
+ //
133
+ // NOTE: it is important to do this only if EPSILON is not in `nextTokens`,
134
+ // which means that we cannot bring the better special syntax-fragile-ident
135
+ // in all cases. Reason: high performance impact of the alternative,
136
+ // i.e. calling method Parser#isExpectedToken() = invoking the ATN
137
+ // interpreter to see behind EPSILON.
138
+ const identType = recognizer.constructor.Identifier;
139
+ if (keywordRegexp.test( token.text ) && nextTokens.contains( identType )) {
140
+ recognizer.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text },
141
+ '$(ID) is a reserved name here - write $(DELIMITED) instead if you want to use it' );
142
+ token.type = identType; // make next ANTLR decision assume identifier
143
+ return;
144
+ }
145
+
129
146
  if (recognizer._ctx._sync === 'nop')
130
147
  return;
131
148
  switch (s.stateType) {
132
- case ATNState.BLOCK_START:
133
- case ATNState.STAR_BLOCK_START:
134
- case ATNState.PLUS_BLOCK_START:
135
- case ATNState.STAR_LOOP_ENTRY:
149
+ case ATNState.BLOCK_START: // 3
150
+ case ATNState.STAR_BLOCK_START: // 5
151
+ case ATNState.PLUS_BLOCK_START: // 4
152
+ case ATNState.STAR_LOOP_ENTRY: // 10
136
153
  // report error and recover if possible
137
154
  if ( token.text !== '}' && // do not just delete a '}'
138
155
  this.singleTokenDeletion(recognizer) !== null) { // also calls reportUnwantedToken
@@ -145,8 +162,8 @@ function sync( recognizer ) {
145
162
  }
146
163
  throw new InputMismatchException(recognizer);
147
164
 
148
- case ATNState.PLUS_LOOP_BACK:
149
- case ATNState.STAR_LOOP_BACK: {
165
+ case ATNState.PLUS_LOOP_BACK: // 11
166
+ case ATNState.STAR_LOOP_BACK: { // 9
150
167
  // TODO: do not delete a '}'
151
168
  this.reportUnwantedToken(recognizer);
152
169
  const expecting = new IntervalSet.IntervalSet();
@@ -425,7 +442,8 @@ function getExpectedTokensForMessage( recognizer, offendingToken, deadEnds ) {
425
442
  }
426
443
  else if (offendingToken && recognizer.$nextTokensContext &&
427
444
  offendingToken === recognizer.$nextTokensToken) {
428
- // We have a state (via sync()) with more "expecting" for the same token
445
+ // Before exiting a rule, we had a state (via sync()) with a bigger
446
+ // "expecting set" for the same token
429
447
  ll1._LOOK( atn.states[recognizer.$nextTokensState], null,
430
448
  predictionContext( atn, recognizer.$nextTokensContext ),
431
449
  expected, lookBusy, calledRules, true, true );
@@ -11,14 +11,17 @@ const { ATNState } = require('antlr4/atn/ATNState');
11
11
  const { dictAdd, dictAddArray } = require('../base/dictionaries');
12
12
  const locUtils = require('../base/location');
13
13
  const { parseDocComment } = require('./docCommentParser');
14
+ const { parseMultiLineStringLiteral } = require('./multiLineStringParser');
14
15
  const { functionsWithoutParens, specialFunctions } = require('../compiler/builtins');
15
16
 
16
17
 
17
18
  // Push message `msg` with location `loc` to array of errors:
18
19
  function _message( parser, severity, id, loc, ...args ) {
19
20
  const msg = parser.$messageFunctions[severity]; // set in antlrParser.js
20
- return msg( id,
21
- (loc instanceof antlr4.CommonToken) ? parser.tokenLocation(loc) : loc, ...args );
21
+ if (loc instanceof antlr4.CommonToken) {
22
+ loc = parser.tokenLocation(loc);
23
+ }
24
+ return msg( id, loc, ...args );
22
25
  }
23
26
 
24
27
  // Class which is to be used as grammar option with
@@ -49,6 +52,8 @@ GenericAntlrParser.prototype = Object.assign(
49
52
  attachLocation,
50
53
  startLocation,
51
54
  tokenLocation,
55
+ valueWithTokenLocation,
56
+ previousTokenAtLocation,
52
57
  combinedLocation,
53
58
  surroundByParens,
54
59
  unaryOpForParens,
@@ -82,6 +87,7 @@ GenericAntlrParser.prototype = Object.assign(
82
87
  isStraightBefore,
83
88
  meltKeywordToIdentifier,
84
89
  prepareGenericKeywords,
90
+ parseMultiLineStringLiteral,
85
91
  constructor: GenericAntlrParser, // keep this last
86
92
  }
87
93
  );
@@ -245,9 +251,12 @@ function prepareGenericKeywords( pathItem ) {
245
251
  // TODO: If not just at the beginning, we need a stack for $genericKeywords,
246
252
  // as we can have nested special functions
247
253
  this.$genericKeywords.argFull = Object.keys( spec );
254
+ // @ts-ignore
248
255
  const token = this.getCurrentToken() || { text: '' };
249
- if (spec[token.text.toUpperCase()] === 'argFull')
256
+ if (spec[token.text.toUpperCase()] === 'argFull') {
257
+ // @ts-ignore
250
258
  token.type = this.constructor.GenericArgFull;
259
+ }
251
260
  }
252
261
 
253
262
  // Attach location matched by current rule to node `art`. If a location is
@@ -285,25 +294,75 @@ function startLocation( token = this._ctx.start ) {
285
294
  *
286
295
  * @param {object} token
287
296
  * @param {object} endToken
288
- * @param {any} val
297
+ * @return {CSN.Location}
289
298
  */
290
- function tokenLocation( token, endToken, val ) {
299
+ function tokenLocation( token, endToken = null ) {
291
300
  if (!token)
292
301
  return undefined;
293
302
  if (!endToken) // including null
294
303
  endToken = token;
304
+
295
305
  /** @type {CSN.Location} */
296
- const r = {
306
+ const loc = {
297
307
  file: this.filename,
298
308
  line: token.line,
299
309
  col: token.column + 1,
300
- // we only have single-line tokens
310
+ // Default for single line tokens
301
311
  endLine: endToken.line,
302
312
  endCol: endToken.stop - endToken.start + endToken.column + 2, // after the last char (special for EOF?)
303
- };
304
- if (val !== undefined)
305
- return { location: r, val };
306
- return r;
313
+ }
314
+
315
+ // This check is done for performance reason. No need to access a token's
316
+ // data if we know that it spans only one single line.
317
+ const isMultiLineToken = (
318
+ endToken.type === this.constructor.DocComment ||
319
+ endToken.type === this.constructor.String ||
320
+ endToken.type === this.constructor.UnterminatedLiteral
321
+ );
322
+ if (isMultiLineToken) {
323
+ // Count the number of newlines in the token.
324
+ const source = endToken.source[1].data;
325
+ let newLineCount = 0;
326
+ let lastNewlineIndex = endToken.start;
327
+ for (let i = endToken.start; i < endToken.stop; i++) {
328
+ // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
329
+ // because ANTLR only uses LF for line break detection.
330
+ if (source[i] === 10) { // code point of '\n'
331
+ newLineCount++;
332
+ lastNewlineIndex = i;
333
+ }
334
+ }
335
+ if (newLineCount > 0) {
336
+ loc.endLine = endToken.line + newLineCount;
337
+ loc.endCol = endToken.stop - lastNewlineIndex + 1;
338
+ }
339
+ }
340
+
341
+ return loc;
342
+ }
343
+
344
+ /**
345
+ * Return `val` with the location of `token`. If `endToken` is provided, use its end
346
+ * location as end location in the result.
347
+ *
348
+ * @param {object} startToken
349
+ * @param {object} endToken
350
+ * @param {any} val
351
+ */
352
+ function valueWithTokenLocation( val, startToken, endToken = null ) {
353
+ if (!startToken)
354
+ return undefined;
355
+ const loc = this.tokenLocation( startToken, endToken );
356
+ return { location: loc, val };
357
+ }
358
+
359
+ function previousTokenAtLocation( location ) {
360
+ let k = -1;
361
+ let token = this._input.LT(k);
362
+ while (token.line > location.line ||
363
+ token.line === location.line && token.column >= location.col)
364
+ token = this._input.LT(--k);
365
+ return (token.line === location.line && token.column + 1 === location.col) && token;
307
366
  }
308
367
 
309
368
  // Create a location with location properties `filename` and `start` from
@@ -342,16 +401,20 @@ function unaryOpForParens( query, val ) {
342
401
  // - would influence the prediction, probably even induce adaptivePredict() calls,
343
402
  // - is only slightly "more declarative" in the grammar.
344
403
  function docComment( node ) {
345
- if (!this.options.docComment)
346
- return;
347
404
  const token = this._input.getHiddenTokenToLeft( this.constructor.DocComment );
348
405
  if (!token)
349
406
  return;
407
+
408
+ // This token is actually used by / assigned to an artifact.
409
+ token.isUsed = true;
410
+
411
+ if (!this.options.docComment)
412
+ return;
350
413
  if (node.doc) {
351
414
  this.warning( 'syntax-duplicate-doc-comment', token, {},
352
415
  'Repeated doc comment - previous doc is replaced' );
353
416
  }
354
- node.doc = this.tokenLocation( token, token, parseDocComment( token.text ) );
417
+ node.doc = this.valueWithTokenLocation( parseDocComment( token.text ), token );
355
418
  }
356
419
 
357
420
  // Classify token (identifier category) for implicit names,
@@ -435,20 +498,24 @@ function valuePathAst( ref ) {
435
498
  path.length = 1;
436
499
  }
437
500
  const { args, id, location } = path[0];
438
- if (args) {
439
- if (path[0].$syntax !== ':')
440
- return { op: { location, val: 'call' }, func: ref, location: ref.location, args };
441
- }
442
- else if (!path[0].$delimited && functionsWithoutParens.includes( id.toUpperCase() )) {
443
- return { op: { location, val: 'call' }, func: ref, location: ref.location };
444
- }
445
- return ref;
501
+ if (args
502
+ ? path[0].$syntax === ':'
503
+ : path[0].$delimited || !functionsWithoutParens.includes( id.toUpperCase() ))
504
+ return ref;
505
+
506
+ const implicit = this.previousTokenAtLocation( location );
507
+ if (implicit && implicit.isIdentifier)
508
+ implicit.isIdentifier = 'func';
509
+ const op = { location, val: 'call' };
510
+ return (args)
511
+ ? { op, func: ref, location: ref.location, args }
512
+ : { op, func: ref, location: ref.location };
446
513
  }
447
514
 
448
515
  // If a '-' is directly before an unsigned number, consider it part of the number;
449
516
  // otherwise (including for '+'), represent it as extra unary prefix operator.
450
517
  function signedExpression( signToken, expr ) {
451
- const sign = this.tokenLocation( signToken, undefined, signToken.text );
518
+ const sign = this.valueWithTokenLocation( signToken.text, signToken );
452
519
  const nval =
453
520
  (signToken.text === '-' &&
454
521
  expr && // expr may be null if `-` rule can't be parsed
@@ -498,8 +565,16 @@ function numberLiteral( token, sign, text = token.text ) {
498
565
  function quotedLiteral( token, literal ) {
499
566
  /** @type {CSN.Location} */
500
567
  const location = this.tokenLocation( token );
501
- const pos = token.text.search( '\'' ) + 1; // pos of char after quote
502
- const val = token.text.slice( pos, -1 ).replace( /''/g, '\'' );
568
+ let pos;
569
+ let val;
570
+
571
+ if (token.text.startsWith('`')) {
572
+ val = this.parseMultiLineStringLiteral(token);
573
+ literal = 'string';
574
+ } else {
575
+ pos = token.text.search( '\'' ) + 1; // pos of char after quote
576
+ val = token.text.slice( pos, -1 ).replace( /''/g, '\'' );
577
+ }
503
578
 
504
579
  if (!literal)
505
580
  literal = token.text.slice( 0, pos - 1 ).toLowerCase();
@@ -529,6 +604,7 @@ function quotedLiteral( token, literal ) {
529
604
  };
530
605
 
531
606
  function atChar(i) {
607
+ // Is only used with single-line strings.
532
608
  return location.col + pos + i;
533
609
  }
534
610
  }
@@ -601,7 +677,7 @@ function addDef( parent, env, kind, name, annos, props, location ) {
601
677
  // which could be tested in name search (then no undefined-ref error)
602
678
  return art;
603
679
  }
604
- else if (env === 'artifacts') {
680
+ else if (env === 'artifacts' || env === 'vocabularies') {
605
681
  dictAddArray( parent[env], art.name.id, art );
606
682
  }
607
683
  else if (kind || this.options.parseOnly) {
@@ -671,7 +747,7 @@ function assignProps( target, annos = [], props = null, location = null) {
671
747
  for (const key in props) {
672
748
  let val = props[key];
673
749
  if (val instanceof antlr4.CommonToken)
674
- val = this.tokenLocation( val, undefined, true);
750
+ val = this.valueWithTokenLocation( true, val);
675
751
  // only copy properties which are not undefined, null, {} or []
676
752
  if (val != null &&
677
753
  (typeof val !== 'object' ||
@@ -685,15 +761,15 @@ function assignProps( target, annos = [], props = null, location = null) {
685
761
 
686
762
  // Create AST node for prefix operator `op` and arguments `args`
687
763
  function createPrefixOp( token, args ) {
688
- const op = this.tokenLocation( token, undefined, token.text.toLowerCase() );
764
+ const op = this.valueWithTokenLocation( token.text.toLowerCase(), token );
689
765
  return { op, args, location: this.combinedLocation( op, args[args.length - 1] ) };
690
766
  }
691
767
 
692
768
  // Create AST node for binary operator `op` and arguments `args`
693
769
  function leftAssocBinaryOp( left, opToken, eToken, right, extraProp = 'quantifier' ) {
694
- const op = this.tokenLocation( opToken, undefined, opToken.text.toLowerCase() );
770
+ const op = this.valueWithTokenLocation( opToken.text.toLowerCase() , opToken);
695
771
  const extra = eToken
696
- ? this.tokenLocation( eToken, undefined, eToken.text.toLowerCase() )
772
+ ? this.valueWithTokenLocation( eToken.text.toLowerCase(), eToken )
697
773
  : undefined;
698
774
  if (!left.$parens &&
699
775
  (left.op && left.op.val) === (op && op.val) &&