@sap/cds-compiler 4.0.2 → 4.2.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 (101) hide show
  1. package/CHANGELOG.md +200 -5
  2. package/bin/cdsc.js +18 -15
  3. package/doc/CHANGELOG_BETA.md +16 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +33 -13
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +25 -25
  8. package/lib/base/location.js +6 -7
  9. package/lib/base/message-registry.js +123 -42
  10. package/lib/base/messages.js +18 -10
  11. package/lib/base/model.js +43 -10
  12. package/lib/checks/defaultValues.js +6 -6
  13. package/lib/checks/elements.js +11 -10
  14. package/lib/checks/foreignKeys.js +0 -5
  15. package/lib/checks/manyNavigations.js +33 -0
  16. package/lib/checks/onConditions.js +22 -14
  17. package/lib/checks/queryNoDbArtifacts.js +132 -73
  18. package/lib/checks/selectItems.js +4 -55
  19. package/lib/checks/sql-snippets.js +15 -4
  20. package/lib/checks/types.js +3 -3
  21. package/lib/checks/utils.js +4 -3
  22. package/lib/checks/validator.js +3 -1
  23. package/lib/compiler/.eslintrc.json +2 -1
  24. package/lib/compiler/assert-consistency.js +71 -40
  25. package/lib/compiler/base.js +7 -2
  26. package/lib/compiler/builtins.js +40 -41
  27. package/lib/compiler/checks.js +415 -367
  28. package/lib/compiler/classes.js +62 -0
  29. package/lib/compiler/cycle-detector.js +9 -9
  30. package/lib/compiler/define.js +124 -90
  31. package/lib/compiler/extend.js +115 -88
  32. package/lib/compiler/finalize-parse-cdl.js +26 -25
  33. package/lib/compiler/generate.js +57 -49
  34. package/lib/compiler/index.js +56 -56
  35. package/lib/compiler/kick-start.js +10 -7
  36. package/lib/compiler/moduleLayers.js +1 -1
  37. package/lib/compiler/populate.js +180 -144
  38. package/lib/compiler/propagator.js +10 -9
  39. package/lib/compiler/resolve.js +321 -246
  40. package/lib/compiler/shared.js +812 -433
  41. package/lib/compiler/tweak-assocs.js +114 -50
  42. package/lib/compiler/utils.js +241 -46
  43. package/lib/edm/.eslintrc.json +40 -1
  44. package/lib/edm/annotations/genericTranslation.js +721 -707
  45. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  46. package/lib/edm/csn2edm.js +389 -378
  47. package/lib/edm/edm.js +679 -770
  48. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  49. package/lib/edm/edmInboundChecks.js +29 -27
  50. package/lib/edm/edmPreprocessor.js +689 -648
  51. package/lib/edm/edmUtils.js +279 -300
  52. package/lib/gen/Dictionary.json +34 -10
  53. package/lib/gen/language.checksum +1 -1
  54. package/lib/gen/language.interp +1 -1
  55. package/lib/gen/languageParser.js +2857 -2856
  56. package/lib/json/from-csn.js +77 -51
  57. package/lib/json/to-csn.js +15 -15
  58. package/lib/language/antlrParser.js +2 -1
  59. package/lib/language/genericAntlrParser.js +52 -43
  60. package/lib/language/language.g4 +61 -64
  61. package/lib/language/multiLineStringParser.js +2 -0
  62. package/lib/main.d.ts +65 -0
  63. package/lib/model/csnRefs.js +37 -19
  64. package/lib/model/csnUtils.js +51 -18
  65. package/lib/model/revealInternalProperties.js +30 -22
  66. package/lib/modelCompare/compare.js +149 -41
  67. package/lib/modelCompare/utils/filter.js +55 -25
  68. package/lib/optionProcessor.js +21 -9
  69. package/lib/render/manageConstraints.js +20 -17
  70. package/lib/render/toCdl.js +63 -23
  71. package/lib/render/toHdbcds.js +2 -2
  72. package/lib/render/toRename.js +4 -9
  73. package/lib/render/toSql.js +82 -35
  74. package/lib/render/utils/common.js +11 -9
  75. package/lib/render/utils/unique.js +52 -0
  76. package/lib/transform/db/applyTransformations.js +62 -21
  77. package/lib/transform/db/assertUnique.js +7 -8
  78. package/lib/transform/db/associations.js +2 -2
  79. package/lib/transform/db/cdsPersistence.js +9 -9
  80. package/lib/transform/db/constraints.js +47 -17
  81. package/lib/transform/db/expansion.js +138 -68
  82. package/lib/transform/db/flattening.js +98 -30
  83. package/lib/transform/db/rewriteCalculatedElements.js +20 -14
  84. package/lib/transform/db/temporal.js +1 -1
  85. package/lib/transform/db/transformExists.js +8 -7
  86. package/lib/transform/db/views.js +73 -33
  87. package/lib/transform/draft/db.js +11 -9
  88. package/lib/transform/draft/odata.js +1 -1
  89. package/lib/transform/{forOdataNew.js → forOdata.js} +10 -7
  90. package/lib/transform/forRelationalDB.js +148 -136
  91. package/lib/transform/localized.js +92 -54
  92. package/lib/transform/odata/toFinalBaseType.js +3 -3
  93. package/lib/transform/{transformUtilsNew.js → transformUtils.js} +13 -111
  94. package/lib/transform/translateAssocsToJoins.js +14 -28
  95. package/lib/utils/file.js +7 -7
  96. package/lib/utils/moduleResolve.js +210 -121
  97. package/lib/utils/objectUtils.js +1 -1
  98. package/package.json +5 -5
  99. package/share/messages/check-proper-type-of.md +1 -1
  100. package/share/messages/{check-proper-type.md → def-missing-type.md} +3 -5
  101. package/share/messages/message-explanations.json +1 -1
@@ -0,0 +1,62 @@
1
+ // Base classes used as prototypes for XSN definitions, elements, etc.
2
+ // The goal is to have named classes that can be seen in performance analyses, e.g.
3
+ // by using the [DeOpt Explorer][1].
4
+ // All classes should also be constructible using `{ __proto__: Class, …}`, i.e.
5
+ // their constructors must not do anything besides assigning properties.
6
+ //
7
+ // Refer to these resources:
8
+ // - <https://mathiasbynens.be/notes/shapes-ics>
9
+ // - <https://v8.dev/blog/fast-properties>
10
+ //
11
+ // Before adding new properties, evaluate whether it has any performance
12
+ // impact. Too many properties that are rarely used could reduce performance,
13
+ // but too few could lead to inconsistent object shapes for commonly
14
+ // used properties.
15
+ //
16
+ // Use [DeOpt Explorer][1] to see the different object Maps by v8.
17
+ //
18
+ // [1]: https://devblogs.microsoft.com/typescript/introducing-deopt-explorer/
19
+
20
+ 'use strict';
21
+
22
+ class XsnSource {
23
+ kind = 'source';
24
+ location;
25
+ usings = [];
26
+ dependencies = [];
27
+ artifacts = Object.create( null );
28
+ vocabularies = Object.create( null );
29
+ extensions = [];
30
+ }
31
+
32
+ class XsnArtifact {
33
+ location;
34
+ name;
35
+ kind;
36
+ }
37
+
38
+ class XsnName {
39
+ location;
40
+ }
41
+
42
+ class CsnLocation {
43
+ file;
44
+ line;
45
+ col;
46
+ endLine;
47
+ endCol;
48
+ constructor(file, line, col, endLine, endCol) {
49
+ this.file = file;
50
+ this.line = line;
51
+ this.col = col;
52
+ this.endLine = endLine;
53
+ this.endCol = endCol;
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ XsnSource,
59
+ XsnArtifact,
60
+ XsnName,
61
+ CsnLocation,
62
+ };
@@ -30,15 +30,15 @@ function detectCycles( definitions, reportCycle, cbScc ) {
30
30
 
31
31
  for (const name in definitions) {
32
32
  const a = definitions[name];
33
- if (Array.isArray(a))
33
+ if (Array.isArray( a ))
34
34
  a.forEach( strongConnectRec );
35
35
  else
36
36
  strongConnectRec( a );
37
37
  }
38
38
  // now the cleanup
39
- let nodes = Object.getOwnPropertyNames(definitions).map( n => definitions[n] );
39
+ let nodes = Object.getOwnPropertyNames( definitions ).map( n => definitions[n] );
40
40
  while (nodes.length) { // still nodes to cleaned
41
- nodes = cleanup(nodes);
41
+ nodes = cleanup( nodes );
42
42
  }
43
43
  return;
44
44
 
@@ -46,7 +46,7 @@ function detectCycles( definitions, reportCycle, cbScc ) {
46
46
  if (a._scc) // already processed
47
47
  return;
48
48
  while (a)
49
- a = strongConnect(a);
49
+ a = strongConnect( a );
50
50
  }
51
51
 
52
52
  // Try to build a SCC starting from the node `v`.
@@ -60,18 +60,18 @@ function detectCycles( definitions, reportCycle, cbScc ) {
60
60
  onStack: true,
61
61
  depIndex: 0,
62
62
  } );
63
- stack.push(v);
63
+ stack.push( v );
64
64
  // console.log('PUSH: ', v.kind,v.name)
65
65
  }
66
66
  if (!v._deps) // builtins, otherwise forgotten (TODO: assert in --test-mode)
67
- setProp(v, '_deps', []);
67
+ setProp( v, '_deps', [] );
68
68
  // assert( v._scc.onStack );
69
69
 
70
70
  // Now consider successors of v (called w):
71
71
  while (v._scc.depIndex < v._deps.length) {
72
72
  const w = v._deps[v._scc.depIndex++].art;
73
73
  if (!w._scc) { // node has not yet been visited
74
- setProp(w, '_sccCaller', v);
74
+ setProp( w, '_sccCaller', v );
75
75
  // console.log('CALL: ', v._scc.depIndex )
76
76
  return w; // recursive call with w in recursive algorithm
77
77
  }
@@ -112,7 +112,7 @@ function detectCycles( definitions, reportCycle, cbScc ) {
112
112
  function defaultCb( w, v, r ) {
113
113
  for (const dep of w._deps) {
114
114
  if (dep.art._scc.lowlink === w._scc.lowlink) // in same SCC
115
- reportCycle( w, dep.art, dep.location );
115
+ reportCycle( w, dep.art, dep.location, dep.semanticLoc );
116
116
  }
117
117
  return r;
118
118
  }
@@ -127,7 +127,7 @@ function detectCycles( definitions, reportCycle, cbScc ) {
127
127
  delete v._sccCaller;
128
128
  for (const w of v._deps) {
129
129
  if (w.art._scc)
130
- todos.push(w.art);
130
+ todos.push( w.art );
131
131
  }
132
132
  }
133
133
  }
@@ -141,7 +141,8 @@ const {
141
141
  const { compareLayer } = require('./moduleLayers');
142
142
  const { initBuiltins, isInReservedNamespace } = require('./builtins');
143
143
 
144
- const $location = Symbol.for('cds.$location');
144
+ const $location = Symbol.for( 'cds.$location' );
145
+ const $inferred = Symbol.for( 'cds.$inferred' );
145
146
 
146
147
  /**
147
148
  * Export function of this file. Transform argument `sources` = dictionary of
@@ -172,6 +173,7 @@ function define( model ) {
172
173
  initArtifact,
173
174
  initMembers,
174
175
  checkDefinitions, // TODO: remove
176
+ initSelectItems,
175
177
  } );
176
178
 
177
179
  let boundSelfParamType = true; // special `$self` for binding param must still be initialised
@@ -192,11 +194,11 @@ function define( model ) {
192
194
  beta: 'With option $(PROP), beta features and many other newer features are disabled',
193
195
  } );
194
196
  }
195
- model.definitions = Object.create(null);
197
+ model.definitions = Object.create( null );
196
198
  setLink( model, '_entities', [] ); // for entities with includes
197
199
  model.$entity = 0;
198
- model.$compositionTargets = Object.create(null);
199
- model.$collectedExtensions = Object.create(null);
200
+ model.$compositionTargets = Object.create( null );
201
+ model.$collectedExtensions = Object.create( null );
200
202
 
201
203
  initBuiltins( model );
202
204
  const sourceNames = shuffleArray( Object.keys( model.sources ) );
@@ -226,7 +228,7 @@ function define( model ) {
226
228
 
227
229
  let namespace = src.namespace && src.namespace.path;
228
230
  let prefix = namespace ? `${ pathName( namespace ) }.` : '';
229
- if (isInReservedNamespace(prefix)) {
231
+ if (isInReservedNamespace( prefix )) {
230
232
  error( 'reserved-namespace-cds', [ src.namespace.location, src.namespace ], { name: 'cds' },
231
233
  'The namespace $(NAME) is reserved for CDS builtins' );
232
234
  namespace = null;
@@ -239,7 +241,7 @@ function define( model ) {
239
241
  addPathPrefixes( src.artifacts, prefix ); // before addUsing
240
242
  }
241
243
  else if (src.usings || src.namespace) {
242
- src.artifacts = Object.create(null);
244
+ src.artifacts = Object.create( null );
243
245
  }
244
246
  if (src.usings)
245
247
  shuffleArray( src.usings ).forEach( u => addUsing( u, src ) );
@@ -252,11 +254,11 @@ function define( model ) {
252
254
  }
253
255
  else if (src.definitions) { // CSN input
254
256
  prefix = '';
255
- dictForEach( shuffleDict( src.definitions ), v => addDefinition( v, src ) );
257
+ dictForEach( shuffleDict( src.definitions ), def => addDefinition( def, src, prefix ) );
256
258
  }
257
259
  if (src.vocabularies) {
258
260
  if (!model.vocabularies)
259
- model.vocabularies = Object.create(null);
261
+ model.vocabularies = Object.create( null );
260
262
  dictForEach( shuffleDict( src.vocabularies ), v => addVocabulary( v, src, prefix ) );
261
263
  }
262
264
  if (src.extensions) { // requires using to be known!
@@ -264,10 +266,14 @@ function define( model ) {
264
266
  }
265
267
  }
266
268
 
267
- function addDefinition( art, block ) {
269
+ function addDefinition( art, block, prefix ) {
270
+ if (!art.name.absolute) {
271
+ // TODO: art.name.absolute = art.name.id || …
272
+ art.name.absolute = (!art.name.path) ? art.name.id : prefix + pathName( art.name.path );
273
+ }
268
274
  const { absolute } = art.name;
269
275
  // TODO: check reserved, see checkName()/checkLocalizedObjects() of checks.js
270
- if (absolute === 'cds' || isInReservedNamespace(absolute)) {
276
+ if (absolute === 'cds' || isInReservedNamespace( absolute )) {
271
277
  error( 'reserved-namespace-cds', [ art.name.location, art ], { name: 'cds' },
272
278
  'The namespace $(NAME) is reserved for CDS builtins' );
273
279
  const builtin = model.definitions[absolute];
@@ -292,7 +298,7 @@ function define( model ) {
292
298
  function addPathPrefixes( artifacts, prefix ) {
293
299
  for (const name in artifacts) {
294
300
  const d = artifacts[name];
295
- const a = Array.isArray(d) ? d[0] : d;
301
+ const a = Array.isArray( d ) ? d[0] : d;
296
302
  if (!a.name.absolute)
297
303
  a.name.absolute = prefix + name;
298
304
  const index = name.indexOf( '.' );
@@ -357,7 +363,7 @@ function define( model ) {
357
363
  // create using for own namespace:
358
364
  const last = path[path.length - 1];
359
365
  const { id } = last;
360
- if (src.artifacts[id] || last.id.includes('.'))
366
+ if (src.artifacts[id] || last.id.includes( '.' ))
361
367
  // not used as we have a definition/using with that name, or dotted last path id
362
368
  return;
363
369
  src.artifacts[id] = {
@@ -373,8 +379,7 @@ function define( model ) {
373
379
  function addArtifact( art, block, prefix ) {
374
380
  if (art.kind === 'using')
375
381
  return;
376
- art.name.absolute = prefix + pathName( art.name.path );
377
- addDefinition( art, block );
382
+ addDefinition( art, block, prefix );
378
383
  if (art.artifacts) {
379
384
  const p = `${ art.name.absolute }.`;
380
385
  // path prefixes (usings) must be added before extensions in artifacts:
@@ -444,8 +449,12 @@ function define( model ) {
444
449
  function addVocabulary( vocab, block, prefix ) {
445
450
  setLink( vocab, '_block', block );
446
451
  const { name } = vocab;
447
- if (!name.absolute)
448
- name.absolute = prefix + pathName( name.path );
452
+ if (!name.absolute) {
453
+ // TODO: art.name.absolute = vocab.name.id || …
454
+ vocab.name.absolute = (!vocab.name.path)
455
+ ? vocab.name.id
456
+ : prefix + pathName( vocab.name.path );
457
+ }
449
458
  dictAdd( model.vocabularies, name.absolute, vocab );
450
459
  }
451
460
 
@@ -454,8 +463,8 @@ function define( model ) {
454
463
  */
455
464
  function addI18nBlocks() {
456
465
  // TODO: the sequence should be in sync with extend / annotate / future $sources
457
- const sortedSources = Object.keys(model.sources)
458
- .filter(name => !!model.sources[name].i18n)
466
+ const sortedSources = Object.keys( model.sources )
467
+ .filter( name => !!model.sources[name].i18n )
459
468
  .sort( (a, b) => compareLayer( model.sources[a], model.sources[b] ) );
460
469
 
461
470
  if (sortedSources.length === 0)
@@ -550,7 +559,7 @@ function define( model ) {
550
559
  return;
551
560
  for (const name in src.artifacts) {
552
561
  const entry = src.artifacts[name];
553
- if (!Array.isArray(entry)) // no local name duplicate
562
+ if (!Array.isArray( entry )) // no local name duplicate
554
563
  continue;
555
564
  for (const decl of entry) {
556
565
  if (!decl.$duplicates) { // do not have two duplicate messages
@@ -574,7 +583,7 @@ function define( model ) {
574
583
  if (!art.query)
575
584
  return;
576
585
  art.$queries = [];
577
- setLink( art, '_from', [] ); // for sequence of resolve steps
586
+ setLink( art, '_from', [] ); // for sequence of resolve steps - TODO: remove
578
587
  if (!setLink( art, '_leadingQuery', initQueryExpression( art.query, art ) ) )
579
588
  return; // null or undefined in case of parse error
580
589
  // if (art._leadingQuery !== art.$queries [0]) throw Error('FOO');
@@ -596,7 +605,7 @@ function define( model ) {
596
605
  function initArtifactParentLink( art, definitions ) {
597
606
  setLink( art, '_parent', null );
598
607
  const { absolute } = art.name;
599
- const dot = absolute.lastIndexOf('.');
608
+ const dot = absolute.lastIndexOf( '.' );
600
609
  if (dot < 0)
601
610
  return;
602
611
  art.name.id = absolute.substring( dot + 1 ); // XSN TODO: remove name.id for artifacts
@@ -610,7 +619,7 @@ function define( model ) {
610
619
  }
611
620
  setLink( art, '_parent', parent );
612
621
  if (!parent._subArtifacts)
613
- setLink( parent, '_subArtifacts', Object.create(null) );
622
+ setLink( parent, '_subArtifacts', Object.create( null ) );
614
623
  if (art.$duplicates !== true) // no redef or "first def"
615
624
  parent._subArtifacts[absolute.substring( dot + 1 )] = art; // not dictAdd()
616
625
  }
@@ -631,7 +640,7 @@ function define( model ) {
631
640
  setLink( self, '_parent', art );
632
641
  setLink( self, '_main', art ); // used on main artifact
633
642
  setLink( self, '_origin', art );
634
- art.$tableAliases = Object.create(null);
643
+ art.$tableAliases = Object.create( null );
635
644
  art.$tableAliases[selfname] = self;
636
645
  }
637
646
 
@@ -695,13 +704,17 @@ function define( model ) {
695
704
  for (const q of query.args.slice(1))
696
705
  initQueryExpression( q, art );
697
706
  setLink( query, '_leadingQuery', leading );
698
- if (leading && query.orderBy) {
699
- if (leading.$orderBy)
707
+ if (leading) {
708
+ if (query.orderBy) {
709
+ leading.$orderBy ??= [ ];
700
710
  leading.$orderBy.push( ...query.orderBy );
701
- else
702
- leading.$orderBy = [ ...query.orderBy ];
711
+ }
712
+ if (query.limit) {
713
+ leading.$limit ??= [ ];
714
+ leading.$limit.push( query.limit );
715
+ }
703
716
  }
704
- // ORDER BY to be evaluated in leading query (LIMIT is literal)
717
+ // ORDER BY and LIMIT to be evaluated in leading query
705
718
  }
706
719
  else { // with parse error (`select from <EOF>`, `select from E { *, ( select }`)
707
720
  return undefined;
@@ -711,7 +724,7 @@ function define( model ) {
711
724
  function initQuery() {
712
725
  const main = art._main || art;
713
726
  setLink( query, '_$next',
714
- (art.kind === '$tableAlias' ? art._parent._$next : art) );
727
+ (art.kind === '$tableAlias' ? art._parent._$next : art ) );
715
728
  setLink( query, '_block', art._block );
716
729
  query.kind = 'select';
717
730
  query.name = { location: query.location };
@@ -735,7 +748,7 @@ function define( model ) {
735
748
  return;
736
749
  if (!table.name) {
737
750
  const last = table.path[table.path.length - 1];
738
- const dot = last?.id?.lastIndexOf('.');
751
+ const dot = last?.id?.lastIndexOf( '.' );
739
752
  const id = (dot >= 0) ? last.id.substring( dot + 1 ) : last.id || '';
740
753
  // TODO: if we have too much time, we can calculate the real location with '.'
741
754
  table.name = { $inferred: 'as', id, location: last.location };
@@ -788,6 +801,7 @@ function define( model ) {
788
801
  // (internal) query, should only be relevant for --raw-output, not for
789
802
  // user messages or references - TODO: correct if join on left?
790
803
  table.name.param = aliases[1] || aliases[0] || '<unknown>';
804
+ setLink( table, '_user', query ); // TODO: do not set kind/name
791
805
  setLink( table, '_$next', query._$next );
792
806
  // TODO: probably set this to query if we switch to name restriction in JOIN
793
807
  }
@@ -802,7 +816,7 @@ function define( model ) {
802
816
  if (tableAlias.$inferred === '$internal') {
803
817
  const semanticLoc = tableAlias.query?.name ? tableAlias.query : tableAlias;
804
818
  error( 'name-missing-alias', [ tableAlias.location, semanticLoc ],
805
- { '#': 'duplicate', code: 'as ‹alias›' });
819
+ { '#': 'duplicate', code: 'as ‹alias›' } );
806
820
  }
807
821
  else {
808
822
  error( 'duplicate-definition', [ loc, table ], { name, '#': 'alias' } );
@@ -838,7 +852,7 @@ function define( model ) {
838
852
 
839
853
  function initExprForQuery( expr, query ) {
840
854
  // TODO: use traverseExpr()
841
- if (Array.isArray(expr)) { // TODO: old-style $parens ?
855
+ if (Array.isArray( expr )) { // TODO: old-style $parens ?
842
856
  expr.forEach( e => initExprForQuery( e, query ) );
843
857
  }
844
858
  else if (!expr) {
@@ -848,13 +862,13 @@ function define( model ) {
848
862
  initQueryExpression( expr.query, query );
849
863
  }
850
864
  else if (expr.args) {
851
- const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
865
+ const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
852
866
  args.forEach( e => initExprForQuery( e, query ) );
853
867
  }
854
868
  else if (expr.path && expr.$expected === 'exists') {
855
869
  // TODO: does really the parser has to set $expected?
856
870
  expr.$expected = 'approved-exists';
857
- approveExistsInChildren(expr);
871
+ approveExistsInChildren( expr );
858
872
  }
859
873
  }
860
874
 
@@ -880,9 +894,9 @@ function define( model ) {
880
894
  }
881
895
  }
882
896
 
883
- function initSelectItems( parent, columns, user ) {
884
- // TODO: forbid expand/inline with :param, global:true, in ref-where, outside queries (CSN), ...
885
- let wildcard = null;
897
+ function initSelectItems( parent, columns, user, inExtension = false ) {
898
+ let wildcard = !!inExtension; // no `extend with columns { * }`
899
+ // TODO: forbid expand/inline in ref-where, outside queries (CSN), ...
886
900
  let hasItems = false;
887
901
  for (const col of columns || parent.expand || parent.inline || []) {
888
902
  if (!col) // parse error
@@ -898,6 +912,11 @@ function define( model ) {
898
912
  if (!wildcard) {
899
913
  wildcard = col;
900
914
  }
915
+ else if (wildcard === true) { // in `extend … with columns {…}`
916
+ error( 'ext-unexpected-wildcard', [ col.location, parent ], { code: '*' },
917
+ 'Unexpected $(CODE) (wildcard) in an extension' );
918
+ col.val = null; // do not consider it for expandWildcard()
919
+ }
901
920
  else {
902
921
  // a late syntax error (this code also runs with parse-cdl), i.e.
903
922
  // no semantic loc (wouldn't be available for expand/inline anyway)
@@ -917,7 +936,8 @@ function define( model ) {
917
936
  }
918
937
  // Either expression (value), expand or new association (target && type)
919
938
  else if (col.value || col.expand || (col.target && col.type)) {
920
- setLink( col, '_block', parent._block );
939
+ if (!col._block)
940
+ setLink( col, '_block', parent._block );
921
941
  if (col.inline) { // `@anno elem.{ * }` does not work
922
942
  if (col.doc) {
923
943
  warning( 'syntax-ignoring-anno', [ col.doc.location, col ],
@@ -925,30 +945,25 @@ function define( model ) {
925
945
  }
926
946
  // col.$annotations no available for CSN input, have to search.
927
947
  // Warning about first annotation should be enough to avoid spam.
928
- const firstAnno = Object.keys(col).find(key => key.startsWith('@'));
948
+ const firstAnno = Object.keys( col ).find( key => key.startsWith( '@' ) );
929
949
  if (firstAnno) {
930
950
  warning( 'syntax-ignoring-anno', [ col[firstAnno].name.location, col ],
931
951
  { code: '.{ ‹inline› }' } );
932
952
  }
933
953
  }
934
954
  // TODO: allow sub queries? at least in top-level expand without parallel ref
935
- if (columns)
955
+ if (columns && !inExtension) // not (yet) in `extend … with columns {…}`
936
956
  initExprForQuery( col.value, parent );
937
- initSelectItems( col, null, user );
957
+ initSelectItems( col, null, user ); // TODO: use col as user (i.e. remove param)
938
958
  }
939
959
  }
940
960
 
941
- if (hasItems && !wildcard && parent.excludingDict) {
942
- // TODO: Better way to get source file?
943
- let block = parent;
944
- while (block._block)
945
- block = block._block;
946
-
947
- if (block.$frontend === 'cdl') {
948
- warning('query-ignoring-exclude', [ parent.excludingDict[$location], user ],
949
- { prop: '*' },
950
- 'Excluding elements without wildcard $(PROP) has no effect');
951
- }
961
+ if (hasItems && !wildcard && parent.excludingDict && !options.$recompile) {
962
+ // TODO: the SQL backend should probably delete `excluding` when expanding `*`
963
+ // TODO: use `parent` for semantic location, use `-excluding`
964
+ warning( 'query-ignoring-exclude', [ parent.excludingDict[$location], user ],
965
+ { prop: '*' },
966
+ 'Excluding elements without wildcard $(PROP) has no effect' );
952
967
  }
953
968
  }
954
969
 
@@ -975,11 +990,11 @@ function define( model ) {
975
990
  exprOrPathElement.$expected = 'approved-exists';
976
991
  // Drill down
977
992
  if (exprOrPathElement.args)
978
- exprOrPathElement.args.forEach(elem => approveExistsInChildren(elem));
993
+ exprOrPathElement.args.forEach( elem => approveExistsInChildren( elem ) );
979
994
  else if (exprOrPathElement.where && exprOrPathElement.where.args)
980
- exprOrPathElement.where.args.forEach(elem => approveExistsInChildren(elem));
995
+ exprOrPathElement.where.args.forEach( elem => approveExistsInChildren( elem ) );
981
996
  else if (exprOrPathElement.path)
982
- exprOrPathElement.path.forEach(elem => approveExistsInChildren(elem));
997
+ exprOrPathElement.path.forEach( elem => approveExistsInChildren( elem ) );
983
998
  }
984
999
  // TODO: we might issue 'expr-unexpected-exists' and 'expr-no-subquery' already in
985
1000
  // define.js (using a to-be-written expression traversal function in utils.js)
@@ -1016,13 +1031,13 @@ function define( model ) {
1016
1031
  const { targetAspect } = obj;
1017
1032
  if (targetAspect) {
1018
1033
  if (obj.foreignKeys) {
1019
- error( 'unexpected-keys-for-composition', [ obj.foreignKeys[$location], construct ],
1034
+ error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ],
1020
1035
  {},
1021
1036
  'A managed aspect composition can\'t have a foreign keys specification' );
1022
1037
  delete obj.foreignKeys; // continuation semantics: not specified
1023
1038
  }
1024
1039
  if (obj.on && !obj.target) {
1025
- error( 'unexpected-on-for-composition', [ obj.on.location, construct ],
1040
+ error( 'type-unexpected-on-condition', [ obj.on.location, construct ],
1026
1041
  {},
1027
1042
  'A managed aspect composition can\'t have a specified ON-condition' );
1028
1043
  delete obj.on; // continuation semantics: not specified
@@ -1056,30 +1071,33 @@ function define( model ) {
1056
1071
 
1057
1072
  function initElementsAsEnum() {
1058
1073
  // in extensions, extended enums are represented as elements
1059
- let firstEnum = null;
1074
+ let hasElement = false;
1060
1075
  for (const n in obj.elements) {
1061
1076
  const e = obj.elements[n];
1062
- if (e.kind === 'element') {
1063
- // An "element" has `$syntax: 'enum'` if it could also be an enum
1064
- if (e.$syntax === 'enum') { // TODO: what about "just name"? (current forbidden)
1065
- e.kind = 'enum';
1066
- firstEnum = firstEnum || e;
1067
- }
1068
- else {
1069
- // We do not want to complain separately about all element properties:
1070
- error( 'ext-unexpected-element', [ e.location, construct ],
1071
- { name: e.name.id, code: 'extend … with enum' },
1072
- // eslint-disable-next-line max-len
1073
- 'Unexpected elements like $(NAME) in an extension for an enum. Additionally, use $(CODE) when extending enums' );
1074
- return;
1075
- }
1077
+ if (e.kind === 'extend')
1078
+ continue;
1079
+ const noVal = e.value?.val === undefined && e.value?.sym === undefined;
1080
+ // TODO: forbid #symbol as enum value
1081
+ if (e.$syntax === 'element' || // `extend … with elements` or `extend with { element … }`
1082
+ noVal && e.$syntax !== 'enum' || // no value in CDL input
1083
+ e.virtual || e.key || e.masked || e.type || e.elements || e.items || e.stored) {
1084
+ // We do not want to complain separately about all element properties:
1085
+ error( 'ext-unexpected-element', [ e.location, construct ],
1086
+ { name: e.name.id, code: 'extend … with enum' },
1087
+ // eslint-disable-next-line max-len
1088
+ 'Unexpected elements like $(NAME) in an extension for an enum. Additionally, use $(CODE) when extending enums' );
1089
+ // Don't emit 'ext-expecting-enum' if this error is emitted.
1090
+ return;
1076
1091
  }
1092
+ e.kind = 'enum';
1093
+ if (noVal || e.$syntax !== 'enum')
1094
+ hasElement = true; // warning with CDL input or `name: {}` in CSN input
1077
1095
  }
1078
- if (firstEnum && block.$frontend !== 'json') {
1079
- // Don't emit this message if `ext-unexpected-element` was already emitted.
1080
- // This message is similar to the one above. In v5/6, we could probably remove the warning
1081
- // and always emit the error.
1082
- warning( 'ext-expecting-enum', [ firstEnum.location, construct ],
1096
+ if (hasElement) {
1097
+ // This message is similar to the one above. In v5/6, we could probably
1098
+ // turn this warning into an error, remove `$syntax: 'element' (also in
1099
+ // language.g4), and use the above `ext-unexpected-element` only for CSN input.
1100
+ warning( 'ext-expecting-enum', [ obj.elements[$location], construct ],
1083
1101
  { code: 'extend … with enum' }, 'Use $(CODE) when extending enums' );
1084
1102
  }
1085
1103
  forEachGeneric( { enum: obj.elements }, 'enum', init );
@@ -1208,18 +1226,25 @@ function define( model ) {
1208
1226
  // - artifacts (CDL-only anyway) only inside [extend] context|service
1209
1227
  if (!dict)
1210
1228
  return false;
1211
- const names = Object.keys( dict );
1212
- if (!names.length) // TODO: re-check - really allow empty dict if no other?
1213
- return false;
1214
1229
  const feature = kindProperties[parent.kind][prop];
1215
1230
  if (feature &&
1216
1231
  (feature === true || construct.kind !== 'extend' || feature( prop, parent )))
1217
1232
  return true;
1218
1233
  const location = dict[$location];
1234
+
1235
+ // TODO: a bit inconsistent = not a simple switch on `prop`…
1219
1236
  if (prop === 'actions') {
1220
- error( 'def-unexpected-actions', [ location, construct ], {},
1221
- 'Actions and functions only exist top-level and for entities' );
1237
+ if (Object.keys( dict ).length) {
1238
+ error( 'def-unexpected-actions', [ location, construct ], {}, // TODO: ext-
1239
+ 'Actions and functions only exist top-level and for entities' ); // or aspects
1240
+ }
1241
+ else {
1242
+ warning( 'ext-ignoring-actions', [ location, construct ], {},
1243
+ 'Actions and functions only exist top-level and for entities' );
1244
+ return false;
1245
+ }
1222
1246
  }
1247
+ //
1223
1248
  else if (parent.kind === 'action' || parent.kind === 'function') {
1224
1249
  error( 'ext-unexpected-action', [ construct.location, construct ], { '#': parent.kind }, {
1225
1250
  std: 'Actions and functions can\'t be extended, only annotated',
@@ -1231,7 +1256,7 @@ function define( model ) {
1231
1256
  if (!feature) {
1232
1257
  // Note: This error can't be triggered at the moment. But as we likely want to
1233
1258
  // allow extensions with params in the future, we keep the code.
1234
- error( 'unexpected-params', [ location, construct ], {},
1259
+ error( 'def-unexpected-params', [ location, construct ], {},
1235
1260
  'Parameters only exist for entities, actions or functions' );
1236
1261
  }
1237
1262
  else {
@@ -1241,10 +1266,15 @@ function define( model ) {
1241
1266
  }
1242
1267
  }
1243
1268
  else if (feature) { // allowed in principle, but not with extend
1244
- if (parent.$inferred === 'include') { // special case for better error message
1269
+ if (!Object.keys( dict ).length) {
1270
+ warning( 'ext-ignoring-elements', [ location, construct ], {},
1271
+ 'Only structures with directly specified elements can be extended by elements' );
1272
+ return false;
1273
+ }
1274
+ else if (parent.$inferred === 'include') { // special case for better error message
1245
1275
  const variant = (construct.enum || construct.elements) ? 'elements' : 'std';
1246
1276
  error( 'ref-expected-direct-structure', [ location, construct ],
1247
- { '#': variant, art: parent });
1277
+ { '#': variant, art: parent } );
1248
1278
  }
1249
1279
  else {
1250
1280
  error( 'extend-type', [ location, construct ], {},
@@ -1252,7 +1282,7 @@ function define( model ) {
1252
1282
  }
1253
1283
  }
1254
1284
  else if (prop === 'elements') {
1255
- error( 'unexpected-elements', [ location, construct ], {},
1285
+ error( 'def-unexpected-elements', [ location, construct ], {},
1256
1286
  'Elements only exist in entities, types or typed constructs' );
1257
1287
  }
1258
1288
  else if (prop === 'columns') {
@@ -1265,19 +1295,23 @@ function define( model ) {
1265
1295
  return construct === parent;
1266
1296
  }
1267
1297
 
1268
- // Return whether the `target` is actually a `targetAspect`
1269
- // TODO: really do that here and not in kick-start.js?
1298
+ /**
1299
+ * Return whether the `target` is actually a `targetAspect`
1300
+ * TODO: really do that here and not in kick-start.js?
1301
+ */
1270
1302
  function targetIsTargetAspect( elem ) {
1271
1303
  const { target } = elem;
1272
1304
  if (target.elements) {
1273
- // TODO: error if CSN has both target.elements and targetAspect.elements -> delete target
1305
+ // TODO: error if CSN has both target.elements and targetAspect.elements
1306
+ // -> delete target
1274
1307
  return true;
1275
1308
  }
1276
1309
  if (elem.targetAspect || options.parseCdl || !isDirectComposition( elem ))
1277
1310
  return false;
1278
1311
  const name = resolveUncheckedPath( target, 'target', elem );
1279
1312
  const aspect = name && model.definitions[name];
1280
- return aspect && (aspect.kind === 'aspect' || aspect.kind === 'type'); // type is sloppy
1313
+ return (aspect?.kind === 'aspect' || aspect?.kind === 'type') && // type is sloppy
1314
+ aspect.elements && !aspect.elements[$inferred];
1281
1315
  }
1282
1316
  }
1283
1317