@sap/cds-compiler 4.3.2 → 4.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 (81) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/api/main.js +14 -24
  3. package/lib/api/options.js +1 -0
  4. package/lib/api/trace.js +38 -0
  5. package/lib/base/location.js +46 -1
  6. package/lib/base/message-registry.js +68 -16
  7. package/lib/base/messages.js +8 -3
  8. package/lib/checks/.eslintrc.json +1 -0
  9. package/lib/checks/actionsFunctions.js +1 -1
  10. package/lib/checks/annotationsOData.js +2 -2
  11. package/lib/checks/selectItems.js +4 -1
  12. package/lib/compiler/assert-consistency.js +3 -2
  13. package/lib/compiler/base.js +1 -1
  14. package/lib/compiler/builtins.js +25 -1
  15. package/lib/compiler/checks.js +6 -5
  16. package/lib/compiler/define.js +12 -10
  17. package/lib/compiler/extend.js +22 -22
  18. package/lib/compiler/finalize-parse-cdl.js +1 -1
  19. package/lib/compiler/generate.js +70 -53
  20. package/lib/compiler/kick-start.js +7 -5
  21. package/lib/compiler/populate.js +31 -22
  22. package/lib/compiler/propagator.js +6 -2
  23. package/lib/compiler/resolve.js +52 -17
  24. package/lib/compiler/shared.js +74 -38
  25. package/lib/compiler/tweak-assocs.js +64 -23
  26. package/lib/compiler/utils.js +40 -23
  27. package/lib/edm/.eslintrc.json +2 -0
  28. package/lib/edm/EdmPrimitiveTypeDefinitions.js +252 -0
  29. package/lib/edm/annotations/edmJson.js +994 -0
  30. package/lib/edm/annotations/genericTranslation.js +75 -421
  31. package/lib/edm/annotations/vocabularyDefinitions.js +160 -0
  32. package/lib/edm/csn2edm.js +12 -5
  33. package/lib/edm/edm.js +14 -73
  34. package/lib/edm/edmPreprocessor.js +6 -0
  35. package/lib/gen/Dictionary.json +187 -16
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +1 -1
  38. package/lib/gen/languageLexer.interp +1 -1
  39. package/lib/gen/languageLexer.js +1129 -671
  40. package/lib/gen/languageParser.js +4285 -4283
  41. package/lib/json/from-csn.js +13 -18
  42. package/lib/json/to-csn.js +11 -6
  43. package/lib/language/antlrParser.js +0 -1
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/errorStrategy.js +95 -30
  46. package/lib/language/genericAntlrParser.js +21 -1
  47. package/lib/main.js +13 -3
  48. package/lib/model/csnRefs.js +42 -8
  49. package/lib/model/csnUtils.js +14 -2
  50. package/lib/model/enrichCsn.js +33 -5
  51. package/lib/model/revealInternalProperties.js +5 -0
  52. package/lib/modelCompare/compare.js +76 -14
  53. package/lib/modelCompare/utils/filter.js +19 -12
  54. package/lib/optionProcessor.js +2 -0
  55. package/lib/render/.eslintrc.json +1 -1
  56. package/lib/render/manageConstraints.js +1 -0
  57. package/lib/render/toHdbcds.js +3 -0
  58. package/lib/render/toRename.js +3 -1
  59. package/lib/render/toSql.js +46 -92
  60. package/lib/render/utils/common.js +76 -0
  61. package/lib/render/utils/delta.js +17 -3
  62. package/lib/sql-identifier.js +1 -1
  63. package/lib/transform/db/.eslintrc.json +1 -0
  64. package/lib/transform/db/applyTransformations.js +30 -4
  65. package/lib/transform/db/associations.js +22 -10
  66. package/lib/transform/db/backlinks.js +6 -2
  67. package/lib/transform/db/expansion.js +2 -2
  68. package/lib/transform/db/transformExists.js +13 -39
  69. package/lib/transform/draft/db.js +14 -3
  70. package/lib/transform/draft/odata.js +5 -18
  71. package/lib/transform/effective/associations.js +46 -15
  72. package/lib/transform/effective/main.js +7 -2
  73. package/lib/transform/effective/misc.js +43 -24
  74. package/lib/transform/effective/queries.js +20 -22
  75. package/lib/transform/effective/types.js +6 -2
  76. package/lib/transform/forOdata.js +5 -2
  77. package/lib/transform/localized.js +1 -1
  78. package/lib/transform/parseExpr.js +73 -21
  79. package/lib/transform/translateAssocsToJoins.js +22 -15
  80. package/lib/utils/term.js +2 -2
  81. package/package.json +2 -1
@@ -44,9 +44,11 @@
44
44
  * Use this message variant instead of the default one.
45
45
  * Allows more precise and detailed error messages.
46
46
  *
47
- * @property {string|string[]|false} [requires]
48
- * If the value is a string, then the given sub-property is required. If 'undefined', then at
49
- * least one property is required. If false the no sub-properties are required.
47
+ * @property {string|string[]|Function|false} [requires]
48
+ * If the value is a(n array of) string, then (one of) the given sub-property is required.
49
+ * If a function, that function issues its own message.
50
+ * If `undefined` (default), then at least one property is required.
51
+ * If false, then no sub-properties are required.
50
52
  *
51
53
  * @property {boolean} [noPrefix]
52
54
  * Only used for '#' at the moment. Signals that the entry should not be used for keys
@@ -117,7 +119,8 @@
117
119
  */
118
120
 
119
121
  const { dictAdd } = require('../base/dictionaries');
120
- const { quotedLiteralPatterns } = require('../compiler/builtins');
122
+ // TODO: move parts of lib/compiler/builtins to some lib/base/…:
123
+ const { isAnnotationExpression, quotedLiteralPatterns } = require('../compiler/builtins');
121
124
  const { CompilerAssertion } = require('../base/error');
122
125
  const { XsnSource, CsnLocation } = require('../compiler/classes');
123
126
 
@@ -171,17 +174,6 @@ const xorGroups = {
171
174
  // quantifiers 'some' and 'any are 'xpr' token strings in CSN v1.0
172
175
  };
173
176
 
174
- /**
175
- * Properties that are required next to `=` to make an annotation value an actual expression
176
- * and not some foreign structure.
177
- *
178
- * @type {string[]}
179
- */
180
- const xprInAnnoProperties = [
181
- 'ref', 'xpr', 'list', 'literal', 'val',
182
- '#', 'func', 'args', 'SELECT', 'SET',
183
- ];
184
-
185
177
  // Functions reading properties which do not count for the message
186
178
  // 'Object in $(PROP) must have at least one property'
187
179
  const functionsOfIrrelevantProps = [ ignore, extra, explicitName ];
@@ -661,7 +653,11 @@ const schema = compileSchema( {
661
653
  '-expr': { // '-expr' and '-' must not exist top-level
662
654
  prop: '@‹anno›',
663
655
  type: object,
664
- optional: [ '=', '#', 'xpr', 'ref', 'val', 'list', 'literal', 'func', 'args', 'param' ],
656
+ optional: [
657
+ '=', '#', 'xpr', 'ref', 'val', 'list',
658
+ 'literal', 'func', 'args', 'param',
659
+ 'cast',
660
+ ],
665
661
  schema: {
666
662
  '=': {
667
663
  type: renameTo( '$tokenTexts', string ),
@@ -1440,7 +1436,6 @@ function annoValue( val, spec ) {
1440
1436
  // An object with `=` is an expression if and only if:
1441
1437
  // - there is exactly one property ('=')
1442
1438
  // - there is at least one other expression property (e.g. "xpr")
1443
- // TODO: Have xprInAnnoProperties centrally for other backends to use as well (toCdl)
1444
1439
  const valKeys = Object.keys( val );
1445
1440
  if (valKeys.length === 1) {
1446
1441
  ++virtualLine;
@@ -1448,7 +1443,7 @@ function annoValue( val, spec ) {
1448
1443
  ++virtualLine;
1449
1444
  return r;
1450
1445
  }
1451
- else if (xprInAnnoProperties.some( prop => val[prop] !== undefined) ) {
1446
+ else if (isAnnotationExpression( val )) {
1452
1447
  const s = schema['@'].schema['-expr'];
1453
1448
  const r = { location: location() };
1454
1449
  Object.assign( r, object( val, s ) );
@@ -45,7 +45,8 @@ const $inferred = Symbol.for('cds.$inferred');
45
45
  const inferredAsGenerated = {
46
46
  autoexposed: 'exposed',
47
47
  'localized-entity': 'localized',
48
- localized: 'localized', // on elements (texts, localized)
48
+ localized: 'localized', // on elements (texts, localized, language)
49
+ // remark: not on 'localize-origin' = other elements of inferred base entity
49
50
  'composition-entity': 'composed', // ('aspect-composition' on element not in CSN)
50
51
  };
51
52
 
@@ -675,9 +676,9 @@ function dollarSyntax( node, csn ) {
675
676
  function ignore() { /* no-op: ignore property */ }
676
677
 
677
678
  function location( loc, csn, xsn ) {
678
- if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' &&
679
- (!xsn.$inferred || !xsn._main)) { // TODO: also for 'select'
680
- // Also include $location for elements in queries (if not via '*')
679
+ if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' && // TODO: also for 'select'
680
+ (!xsn.$inferred || !xsn._main)) {
681
+ // Also include $location for elements in queries (if not via '*' except for autoexposed)
681
682
  addLocation( xsn.name && xsn.name.location || loc, csn );
682
683
  }
683
684
  }
@@ -768,7 +769,8 @@ function definition( art, _csn, _node, prop ) {
768
769
  if (art.kind === 'key') { // foreignkey
769
770
  const key = addExplicitAs( { ref: art.targetElement.path.map( pathItem ) },
770
771
  art.name, neqPath( art.targetElement ) );
771
- addLocation( art.targetElement.location, key );
772
+ if (!art.$inferred)
773
+ addLocation( art.targetElement.location, key );
772
774
  return extra( key, art );
773
775
  }
774
776
  const c = standard( art );
@@ -1191,8 +1193,9 @@ function value( node ) {
1191
1193
  if (!node)
1192
1194
  return true; // `@aBool` short for `@aBool: true`
1193
1195
  if (universalCsn && node.$inferred) {
1196
+ // TODO: return undefined for all values of node.$inferred (except 'NULL')?
1194
1197
  if (node.$inferred === 'prop' || node.$inferred === '$generated' || // via propagator.js
1195
- node.$inferred === 'parent-origin')
1198
+ node.$inferred === 'parent-origin')
1196
1199
  return undefined;
1197
1200
  else if (node.$inferred === 'NULL')
1198
1201
  return null;
@@ -1250,6 +1253,8 @@ function condition( node ) {
1250
1253
  }
1251
1254
 
1252
1255
  function expression( node ) {
1256
+ if (node?.$inferred && (gensrcFlavor || universalCsn || node.$inferred === 'NULL'))
1257
+ return undefined; // Note: No `null` for universal CSN at the moment
1253
1258
  const expr = exprInternal( node, 'no' );
1254
1259
  return (Array.isArray( expr ))
1255
1260
  ? { xpr: flattenInternalXpr( expr, node.op?.val ) }
@@ -126,7 +126,6 @@ function parse( source, filename = '<undefined>.cds',
126
126
  // comment the following 2 lines if you want to output the parser errors directly:
127
127
  parser.messageErrorListener = errorListener;
128
128
  parser._errHandler = new errorStrategy.KeywordErrorStrategy();
129
- parser.match = errorStrategy.match;
130
129
  parser._interp.predictionMode = antlr4.atn.PredictionMode.SLL;
131
130
  // parser._interp.predictionMode = antlr4.atn.PredictionMode.LL_EXACT_AMBIG_DETECTION;
132
131
 
@@ -39,7 +39,7 @@ function parseDocComment( comment ) {
39
39
  // Remove "/***/" and trim white space and asterisks.
40
40
  const content = lines[0]
41
41
  .replace(/^\/[*]{2,}/, '')
42
- .replace(/\*+\/$/, '')
42
+ .replace(/\**\/$/, '') // for `/*****/`, only `/` remains
43
43
  .replace('*\\/', '*/') // escape sequence
44
44
  .trim();
45
45
  return isWhitespaceOrNewLineOnly(content) ? null : content;
@@ -45,27 +45,6 @@ const keywordRegexp = /^[a-zA-Z]+$/; // we don't have keywords with underscore
45
45
  let SEMI = null;
46
46
  let RBRACE = null;
47
47
 
48
- // Match current token against token type `ttype` and consume it if successful.
49
- // Also allow to match keywords as identifiers. This function should be set as
50
- // property `match` to the parser (prototype). See also `recoverInline()`.
51
- function match( ttype ) {
52
- const identType = this.constructor.Identifier;
53
- if (ttype !== identType)
54
- return antlr4.Parser.prototype.match.call( this, ttype );
55
-
56
- const token = this.getCurrentToken();
57
- if (token.type === identType || !keywordRegexp.test( token.text ))
58
- return antlr4.Parser.prototype.match.call( this, ttype );
59
- // This is very likely to be dead code: we do not use a simple Identifier
60
- // without alternatives in the grammar. With alternatives, recoverInline() is
61
- // the place to go. (But this code should work with a changed grammar…)
62
- this.message( 'syntax-unexpected-reserved-word', token,
63
- { code: token.text, delimited: token.text } );
64
- this._errHandler.reportMatch(this);
65
- this.consume();
66
- return token;
67
- }
68
-
69
48
  // Class which adapts ANTLR4s standard error strategy: do something special
70
49
  // with (non-reserved) keywords.
71
50
  //
@@ -85,6 +64,7 @@ class KeywordErrorStrategy extends DefaultErrorStrategy {
85
64
  // TODO: Use actual methods
86
65
  Object.assign( KeywordErrorStrategy.prototype, {
87
66
  sync,
67
+ singleTokenDeletion,
88
68
  reportNoViableAlternative,
89
69
  reportInputMismatch,
90
70
  reportUnwantedToken,
@@ -102,6 +82,7 @@ Object.assign( KeywordErrorStrategy.prototype, {
102
82
  // Attempt to recover from problems in subrules, except if rule has defined a
103
83
  // local variable `_sync` with value 'nop'
104
84
  // TODO: consider performance - see #8800
85
+ // See DefaultErrorStrategy#sync
105
86
  function sync( recognizer ) {
106
87
  // If already recovering, don't try to sync
107
88
  if (this.inErrorRecoveryMode(recognizer))
@@ -176,6 +157,8 @@ function sync( recognizer ) {
176
157
  this.consumeUntil( recognizer, nextTokens );
177
158
  return;
178
159
  }
160
+ // TODO: at least with STAR_LOOP_ENTRY, we might want to do s/th similar as
161
+ // with LOOP_BACK (syncing to “expected tokens” -> the separator)
179
162
  throw new InputMismatchException(recognizer);
180
163
 
181
164
  case ATNState.PLUS_LOOP_BACK: // 11
@@ -184,15 +167,93 @@ function sync( recognizer ) {
184
167
  this.reportUnwantedToken(recognizer);
185
168
  const expecting = new IntervalSet();
186
169
  expecting.addSet(recognizer.getExpectedTokens());
170
+
171
+ // First try some ',' insertion (TODO does not work yet):
172
+ if (trySeparatorInsertion( recognizer, expecting, "','" ))
173
+ return;
174
+
175
+ // We then try syncing only to the loop-cont (`,`) / loop-end (`}`) token set,
176
+ // but only for the current or next line (and not consuming `;`s):
177
+ const prevToken = recognizer.getTokenStream().LT(-1);
178
+ if (token.line <= prevToken.line + 1 && // in same or next line
179
+ this.consumeAndMarkUntil( recognizer, expecting, true ))
180
+ break;
181
+ // console.log(token.text,JSON.stringify(intervalSetToArray(recognizer,expecting)))
182
+
183
+ // If that fails, we also sync to all tokens which are in the follow set of
184
+ // the current rule and all outer rules
187
185
  const whatFollowsLoopIterationOrRule = expecting.addSet(this.getErrorRecoverySet(recognizer));
188
186
  this.consumeUntil(recognizer, whatFollowsLoopIterationOrRule);
189
- break;
187
+ // console.log(JSON.stringify(intervalSetToArray(recognizer,expecting)))
188
+ if (recognizer._ctx._sync === 'recover' || // in start rule: no exception
189
+ nextTokens.contains( recognizer.getTokenStream().LA(1) ))
190
+ return;
191
+ throw new InputMismatchException(recognizer);
190
192
  }
191
193
  default:
192
194
  // do nothing if we can't identify the exact kind of ATN state
193
195
  }
194
196
  }
195
197
 
198
+
199
+ function trySeparatorInsertion( recognizer, expecting, separatorName ) {
200
+ // Remark: this function does not really work, because it is based on
201
+ // singleTokenInsertion, which also does not really work… (see below).
202
+ // But we might improve it in the future…
203
+ const separator = recognizer.literalNames.indexOf( separatorName );
204
+ if (!expecting.contains( separator ))
205
+ return false;
206
+
207
+ const currentSymbolType = recognizer.getTokenStream().LA(1);
208
+ // if current token is consistent with what could come after current
209
+ // ATN state, then we know we're missing a token; error recovery
210
+ // is free to conjure up and insert the missing token
211
+ const { atn } = recognizer._interp;
212
+ const currentState = atn.states[recognizer.state];
213
+ const next = separatorTransition( currentState.transitions, separator ).target;
214
+ // While this is an improvement to the default ANTLR code for
215
+ // singleTokenInsertion(), it still does not help, as we navigate along an
216
+ // epsilon transition, i.e. we still see ',', etc
217
+ const expectingAtLL2 = atn.nextTokens(next, recognizer._ctx);
218
+ if (!expectingAtLL2.contains(currentSymbolType))
219
+ return false;
220
+
221
+ this.reportMissingToken(recognizer);
222
+ return getMissingSymbol( recognizer, separator );
223
+ }
224
+
225
+ function separatorTransition( transitions, separator ) {
226
+ for (const tr of transitions) {
227
+ if (tr.matches( separator ))
228
+ return tr;
229
+ }
230
+ return transitions[0];
231
+ }
232
+
233
+ function singleTokenDeletion( recognizer ) {
234
+ const token = recognizer.getCurrentToken();
235
+ if (!token || token.text === '}')
236
+ return null;
237
+
238
+ const nextTokenType = recognizer.getTokenStream().LA(2);
239
+ const { Number } = recognizer.constructor;
240
+ if (nextTokenType > Number && // next token is Id|Unreserved|IllegalToken
241
+ token.type <= Number) // current token is not
242
+ return null;
243
+
244
+ const expecting = this.getExpectedTokens(recognizer);
245
+ if (!expecting.contains(nextTokenType))
246
+ return null;
247
+
248
+ this.reportUnwantedToken(recognizer);
249
+ recognizer.consume(); // simply delete extra token
250
+ // we want to return the token we're actually matching
251
+ const matchedSymbol = recognizer.getCurrentToken();
252
+ this.reportMatch( recognizer ); // we know current token is correct
253
+ return matchedSymbol;
254
+ }
255
+
256
+
196
257
  // singleTokenInsertion called by recoverInline (called by match / in else),
197
258
  // calls reportMissingToken
198
259
 
@@ -234,14 +295,14 @@ function reportInputMismatch( recognizer, e, deadEnds ) {
234
295
  }
235
296
 
236
297
  // Report unwanted token when the parser `recognizer` tries to recover/sync
237
- function reportUnwantedToken( recognizer ) {
298
+ function reportUnwantedToken( recognizer, expecting ) {
238
299
  if (this.inErrorRecoveryMode(recognizer))
239
300
  return;
240
301
  this.beginErrorCondition(recognizer);
241
302
 
242
303
  const token = recognizer.getCurrentToken();
243
304
  token.$isSkipped = 'offending';
244
- const expecting = this.getExpectedTokensForMessage( recognizer, token );
305
+ expecting ??= this.getExpectedTokensForMessage( recognizer, token );
245
306
  const offending = this.getTokenDisplay( token, recognizer );
246
307
  // Just text variant, no other message id! Would depend on ANTLR-internals
247
308
  const err = recognizer.error( 'syntax-unexpected-token', token,
@@ -323,14 +384,19 @@ function consumeUntil( recognizer, set ) {
323
384
  }
324
385
  }
325
386
 
326
- function consumeAndMarkUntil( recognizer, set ) {
327
- let t = recognizer.getTokenStream().LT(1);
387
+ function consumeAndMarkUntil( recognizer, set, onlyInSameLine ) {
388
+ const stream = recognizer.getTokenStream();
389
+ let t = stream.LT(1);
390
+ const { line } = t;
328
391
  while (t.type !== antlr4.Token.EOF && !set.contains( t.type )) {
392
+ if (onlyInSameLine && (t.line !== line || t.text === ';' || t.text === '}' ))
393
+ return false; // early exit
329
394
  if (!t.$isSkipped)
330
395
  t.$isSkipped = true;
331
396
  recognizer.consume();
332
- t = recognizer.getTokenStream().LT(1);
397
+ t = stream.LT(1);
333
398
  }
399
+ return true;
334
400
  }
335
401
 
336
402
  // As the `match` function of the parser `recognizer` does not allow to check
@@ -363,8 +429,8 @@ function recoverInline( recognizer ) {
363
429
  // Conjure up a missing token during error recovery in parser `recognizer`. If
364
430
  // an identifier is expected, create one.
365
431
  // Think about: we might want to prefer one of '}]);,'.
366
- function getMissingSymbol( recognizer ) {
367
- const expectedTokenType = this.getExpectedTokens(recognizer).first(); // get any element
432
+ function getMissingSymbol( recognizer, expectedTokenType ) {
433
+ expectedTokenType ??= this.getExpectedTokens(recognizer).first(); // get any element
368
434
  const current = recognizer.getCurrentToken();
369
435
  return recognizer.getTokenFactory().create(
370
436
  current.source, // do s/th special if EOF like in DefaultErrorStrategy ?
@@ -556,6 +622,5 @@ function getExpectedTokensForMessage( recognizer, offendingToken, deadEnds ) {
556
622
  }
557
623
 
558
624
  module.exports = {
559
- match,
560
625
  KeywordErrorStrategy,
561
626
  };
@@ -130,6 +130,7 @@ Object.assign(GenericAntlrParser.prototype, {
130
130
  checkTypeFacet,
131
131
  checkTypeArgs,
132
132
  csnParseOnly,
133
+ markAsSkippedUntilEOF,
133
134
  noAssignmentInSameLine,
134
135
  noSemicolonHere,
135
136
  setLocalToken,
@@ -249,6 +250,25 @@ function setLocalTokenForId( offset, tokenNameMap ) {
249
250
  // // throw new antlr4.error.InputMismatchException(this);
250
251
  // }
251
252
 
253
+ function markAsSkippedUntilEOF() {
254
+ let t = this.getCurrentToken();
255
+ if (t.type === antlr4.Token.EOF)
256
+ return;
257
+ if (!t.$isSkipped && !this._errHandler.inErrorRecoveryMode( this )) {
258
+ // If not already done, we should report an error if we do not see EOF. We cannot
259
+ // use match() here, because these would consume tokens without marking them.
260
+ this._errHandler.reportUnwantedToken( this, [ '<EOF>' ] );
261
+ t.$isSkipped = 'offending';
262
+ this.consume();
263
+ t = this.getCurrentToken();
264
+ }
265
+ while (t.type !== antlr4.Token.EOF) {
266
+ t.$isSkipped = true;
267
+ this.consume();
268
+ t = this.getCurrentToken();
269
+ }
270
+ }
271
+
252
272
  function noAssignmentInSameLine() {
253
273
  const t = this.getCurrentToken();
254
274
  if (t.text === '@' && t.line <= this._input.LT(-1).line) {
@@ -928,12 +948,12 @@ function signedExpression( args, expr ) {
928
948
  function numberLiteral( token, sign, text = token.text ) {
929
949
  let location = this.tokenLocation( token );
930
950
  if (sign) {
931
- // TODO: warning for space in between
932
951
  const { endLine, endCol } = location;
933
952
  location = this.startLocation( sign );
934
953
  location.endLine = endLine;
935
954
  location.endCol = endCol;
936
955
  text = sign.text + text;
956
+ this.reportUnexpectedSpace( sign, this.tokenLocation( token ) );
937
957
  }
938
958
 
939
959
  const num = Number.parseFloat( text || '0' ); // not Number.parseInt() !
package/lib/main.js CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  'use strict';
15
15
 
16
+ const { traceApi } = require('./api/trace');
16
17
  const snapi = lazyload('./api/main');
17
18
  const csnUtils = lazyload('./model/csnUtils');
18
19
  const model_api = lazyload('./model/api');
@@ -75,9 +76,18 @@ function parseExpr( cdlSource, filename = '<expr>.cds', options = {} ) {
75
76
  module.exports = {
76
77
  // Compiler
77
78
  version,
78
- compile: (...args) => compiler.compileX(...args).then( toCsn.compactModel ), // main function
79
- compileSync: (filenames, dir, options, fileCache) => toCsn.compactModel( compiler.compileSyncX(filenames, dir, options, fileCache) ), // main function
80
- compileSources: (sourcesDict, options) => toCsn.compactModel( compiler.compileSourcesX(sourcesDict, options) ), // main function
79
+ compile: (filenames, dir, options, fileCache) => { // main function
80
+ traceApi( 'compile', options );
81
+ return compiler.compileX(filenames, dir, options, fileCache).then(toCsn.compactModel);
82
+ },
83
+ compileSync: (filenames, dir, options, fileCache) => { // main function
84
+ traceApi('compileSync', options);
85
+ return toCsn.compactModel(compiler.compileSyncX(filenames, dir, options, fileCache));
86
+ },
87
+ compileSources: (sourcesDict, options) => { // main function
88
+ traceApi('compileSources', options);
89
+ return toCsn.compactModel(compiler.compileSourcesX(sourcesDict, options));
90
+ },
81
91
  compactModel: csn => csn, // for easy v2 migration
82
92
  get CompilationError() {
83
93
  Object.defineProperty(this, 'CompilationError', {
@@ -194,6 +194,7 @@
194
194
  const BUILTIN_TYPE = {};
195
195
  const { locationString } = require('../base/location');
196
196
  const { ModelError, CompilerAssertion } = require('../base/error');
197
+ const { isAnnotationExpression } = require('../compiler/builtins');
197
198
 
198
199
  // Properties in which artifact or members are defined - next property in the
199
200
  // "csnPath" is the name or index of that property; 'args' (its value can be a
@@ -221,6 +222,8 @@ const referenceSemantics = {
221
222
  inline: { lexical: justDollar, dynamic: 'inline' }, // ...using baseEnv
222
223
  ref_where: { lexical: justDollar, dynamic: 'ref-target' }, // ...using baseEnv
223
224
  on: { lexical: justDollar, dynamic: 'query' }, // assoc defs, redirected to
225
+ annotation: { lexical: justDollar, dynamic: 'query' }, // anno top-level `ref`
226
+ annotationExpr: { lexical: justDollar, dynamic: 'query' }, // annotation assignment
224
227
  // there are also 'on_join' and 'on_mixin' with default semantics
225
228
  orderBy_ref: { lexical: query => query, dynamic: 'query' },
226
229
  orderBy_expr: { lexical: query => query, dynamic: 'source' }, // ref in ORDER BY expression
@@ -467,6 +470,16 @@ function csnRefs( csn, universalReady ) {
467
470
  return art[property];
468
471
  }
469
472
 
473
+ function boundActionOrMain( art ) {
474
+ while (art.kind !== 'action' && art.kind !== 'function') {
475
+ const p = getCache( art, '_parent' );
476
+ if (!p)
477
+ return art;
478
+ art = p;
479
+ }
480
+ return art;
481
+ }
482
+
470
483
  function initDefinition( main ) {
471
484
  // TODO: some --test-mode check that the argument is in ‹csn›.definitions ?
472
485
  if (getCache( main, '$queries' ) !== undefined) // already computed
@@ -572,13 +585,16 @@ function csnRefs( csn, universalReady ) {
572
585
  const path = (typeof ref === 'string') ? [ ref ] : ref.ref;
573
586
  if (!Array.isArray( path ))
574
587
  throw new ModelError( 'References must look like {ref:[...]}' );
575
-
576
- const head = pathId( path[0] );
577
588
  if (main) // TODO: improve, for csnpath starting with art
578
589
  initDefinition( main );
579
- if (ref.param)
580
- return resolvePath( path, main.params[head], main, 'param' );
581
590
 
591
+ const head = pathId( path[0] );
592
+ if (ref.param) {
593
+ const boundOrMain = (query || !main.actions || parent === main)
594
+ ? main // shortcut (would also have been return by function)
595
+ : boundActionOrMain( parent );
596
+ return resolvePath( path, boundOrMain.params[head], boundOrMain, 'param' );
597
+ }
582
598
  const semantics = referenceSemantics[refCtx] || {};
583
599
  if (semantics.$initOnly)
584
600
  return undefined;
@@ -644,6 +660,7 @@ function csnRefs( csn, universalReady ) {
644
660
 
645
661
  if (semantics.dynamic === 'query') {
646
662
  // TODO: for ON condition in expand, would need to use cached _element
663
+ // TODO: test and implement - Issue #11792!
647
664
  return resolvePath( path, qcache.elements[head], null, 'query' );
648
665
  }
649
666
  for (const name in qcache.$aliases) {
@@ -1002,7 +1019,7 @@ function startCsnPath( csnPath, csn ) {
1002
1019
  throw new CompilerAssertion( 'References outside definitions and vocabularies not supported yet');
1003
1020
  const art = csn[csnPath[0]][csnPath[1]];
1004
1021
  return {
1005
- index: 2, main: art, parent: null, art,
1022
+ index: 2, main: art, parent: art, art,
1006
1023
  };
1007
1024
  }
1008
1025
 
@@ -1033,10 +1050,23 @@ function analyseCsnPath( csnPath, csn, resolve ) {
1033
1050
  break;
1034
1051
 
1035
1052
  const prop = csnPath[index];
1053
+ if (refCtx === 'annotation' && typeof obj === 'object') {
1054
+ // we do not know yet whether the annotation value is a expression or not →
1055
+ // loop over outer array and records (structure values):
1056
+ if (Array.isArray( obj ) || !isAnnotationExpression( obj )) {
1057
+ obj = obj[prop];
1058
+ continue;
1059
+ }
1060
+ refCtx = 'annotationExpr';
1061
+ }
1036
1062
  // array item, name/index of artifact/member, (named) argument
1037
1063
  if (isName || Array.isArray( obj ) || prop === 'returns') {
1038
1064
  // TODO: call some kind of resolve.setOrigin()
1039
- if (typeof isName === 'string' || prop === 'returns') {
1065
+ if (isName === 'actions') {
1066
+ art = obj[prop];
1067
+ parent = art; // param refs in annos for actions are based on the action, not the entity
1068
+ }
1069
+ else if (typeof isName === 'string' || prop === 'returns') {
1040
1070
  parent = art;
1041
1071
  art = obj[prop];
1042
1072
  }
@@ -1058,7 +1088,7 @@ function analyseCsnPath( csnPath, csn, resolve ) {
1058
1088
  resolve( '', '$init', main );
1059
1089
  main = obj;
1060
1090
  art = obj;
1061
- parent = null;
1091
+ parent = obj;
1062
1092
  }
1063
1093
  isName = prop;
1064
1094
  // if we want to allow auto-redirect of user-provided target with renamed keys:
@@ -1106,7 +1136,11 @@ function analyseCsnPath( csnPath, csn, resolve ) {
1106
1136
  else if (prop === 'orderBy') {
1107
1137
  refCtx = 'orderBy';
1108
1138
  }
1109
- else if (prop !== 'xpr') {
1139
+ else if (prop.charAt(0) === '@') {
1140
+ refCtx = 'annotation';
1141
+ }
1142
+ else if (prop !== 'xpr' && prop !== 'list') {
1143
+ // 'xpr' and 'list' do not change the ref context, all other props do:
1110
1144
  refCtx = prop;
1111
1145
  }
1112
1146
  obj = obj[prop];
@@ -1162,18 +1162,20 @@ function moveAnnotationsAndDoc( sourceNode, targetNode, overwrite = false ) {
1162
1162
  * @todo Does _not_ apply param/action/... annotations.
1163
1163
  *
1164
1164
  * @param {CSN.Model} csn
1165
- * @param {{notFound?: (name: string, index: number) => void, override?: boolean, filter?: (name: string) => boolean}} config
1165
+ * @param {{notFound?: (name: string, index: number) => void, override?: boolean, filter?: (name: string) => boolean, applyToElements?: boolean}} config
1166
1166
  * notFound: Function that is called if the referenced definition can't be found.
1167
1167
  * Second argument is index in `csn.extensions` array.
1168
1168
  * override: Whether to ignore existing annotations.
1169
1169
  * filter: Positive filter. If it returns true, annotations for the referenced artifact
1170
1170
  * will be applied.
1171
+ * applyToElements: Wether to apply annotations to elements or only to artifacts
1171
1172
  */
1172
1173
  function applyAnnotationsFromExtensions( csn, config ) {
1173
1174
  if (!csn.extensions)
1174
1175
  return;
1175
1176
 
1176
1177
  const filter = config.filter || (_name => true);
1178
+ const applyToElements = config.applyToElements ?? true;
1177
1179
  for (let i = 0; i < csn.extensions.length; ++i) {
1178
1180
  const ext = csn.extensions[i];
1179
1181
  const name = ext.annotate || ext.extend;
@@ -1181,7 +1183,10 @@ function applyAnnotationsFromExtensions( csn, config ) {
1181
1183
  const def = csn.definitions[name];
1182
1184
  if (def) {
1183
1185
  moveAnnotationsAndDoc(ext, def, config.override);
1184
- applyAnnotationsToElements(ext, def);
1186
+ if (applyToElements)
1187
+ applyAnnotationsToElements(ext, def);
1188
+ if (Object.keys(ext).length <= 1)
1189
+ csn.extensions[i] = undefined;
1185
1190
  }
1186
1191
  else if (config.notFound) {
1187
1192
  config.notFound(name, i);
@@ -1189,6 +1194,8 @@ function applyAnnotationsFromExtensions( csn, config ) {
1189
1194
  }
1190
1195
  }
1191
1196
 
1197
+ csn.extensions = csn.extensions.filter(ext => ext);
1198
+
1192
1199
  function applyAnnotationsToElements( ext, def ) {
1193
1200
  // Only the definition is arrayed but the extension is not since
1194
1201
  // `items` is not expected in `extensions` by the CSN frontend and not
@@ -1204,8 +1211,13 @@ function applyAnnotationsFromExtensions( csn, config ) {
1204
1211
  if (targetElem) {
1205
1212
  moveAnnotationsAndDoc(sourceElem, targetElem, config.override);
1206
1213
  applyAnnotationsToElements(sourceElem, targetElem);
1214
+ if (Object.keys(sourceElem).length === 0)
1215
+ delete ext.elements[key];
1207
1216
  }
1208
1217
  });
1218
+
1219
+ if (Object.keys(ext.elements).length === 0)
1220
+ delete ext.elements;
1209
1221
  }
1210
1222
  }
1211
1223