@sap/cds-compiler 2.11.2 → 2.11.4

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 (46) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +3 -1
  4. package/bin/cdsc.js +8 -1
  5. package/bin/cdsv2m.js +3 -2
  6. package/lib/api/main.js +2 -16
  7. package/lib/api/options.js +3 -2
  8. package/lib/api/validate.js +7 -1
  9. package/lib/backends.js +3 -5
  10. package/lib/base/keywords.js +3 -2
  11. package/lib/base/message-registry.js +24 -8
  12. package/lib/base/messages.js +15 -9
  13. package/lib/base/optionProcessorHelper.js +1 -1
  14. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  15. package/lib/checks/unknownMagic.js +1 -1
  16. package/lib/compiler/assert-consistency.js +2 -2
  17. package/lib/compiler/builtins.js +34 -15
  18. package/lib/compiler/definer.js +8 -17
  19. package/lib/compiler/index.js +13 -25
  20. package/lib/compiler/resolver.js +89 -23
  21. package/lib/compiler/shared.js +25 -28
  22. package/lib/compiler/utils.js +11 -0
  23. package/lib/gen/language.checksum +1 -1
  24. package/lib/json/to-csn.js +60 -14
  25. package/lib/language/errorStrategy.js +26 -8
  26. package/lib/language/genericAntlrParser.js +2 -1
  27. package/lib/language/language.g4 +6 -3
  28. package/lib/main.d.ts +79 -1
  29. package/lib/model/csnRefs.js +11 -4
  30. package/lib/model/csnUtils.js +2 -107
  31. package/lib/model/enrichCsn.js +33 -35
  32. package/lib/model/revealInternalProperties.js +5 -4
  33. package/lib/model/sortViews.js +8 -1
  34. package/lib/optionProcessor.js +5 -1
  35. package/lib/render/.eslintrc.json +1 -2
  36. package/lib/render/toHdbcds.js +2 -7
  37. package/lib/render/toSql.js +16 -11
  38. package/lib/transform/db/applyTransformations.js +189 -0
  39. package/lib/transform/db/flattening.js +1 -1
  40. package/lib/transform/db/transformExists.js +9 -0
  41. package/lib/transform/db/views.js +89 -42
  42. package/lib/transform/forHanaNew.js +34 -12
  43. package/lib/transform/translateAssocsToJoins.js +3 -3
  44. package/lib/utils/file.js +6 -2
  45. package/package.json +1 -1
  46. package/lib/transform/db/helpers.js +0 -58
@@ -26,7 +26,6 @@ const resolve = require('./resolver');
26
26
  const propagator = require('./propagator');
27
27
  const check = require('./checks');
28
28
 
29
-
30
29
  const { emptyWeakLocation } = require('../base/location');
31
30
  const { createMessageFunctions, deduplicateMessages } = require('../base/messages');
32
31
  const { promiseAllDoNotRejectImmediately } = require('../base/node-helpers');
@@ -144,36 +143,25 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
144
143
  });
145
144
 
146
145
  // Read file `filename` and parse its content, return messages
147
- function readAndParse( filename ) {
146
+ async function readAndParse( filename ) {
147
+ const { sources } = a;
148
148
  if ( filename === false ) // module which has not been found
149
149
  return [];
150
- const rel = a.sources[filename] || path.relative( dir, filename );
150
+ const rel = sources[filename] || path.relative( dir, filename );
151
151
  if (typeof rel === 'object') // already parsed
152
152
  return []; // no further dependency processing
153
153
  // no parallel readAndParse with same resolved filename should read the file,
154
- // also ensure deterministic sequence in a.sources:
155
- a.sources[filename] = { location: { file: rel } };
154
+ // also ensure deterministic sequence in sources:
155
+ sources[filename] = { location: { file: rel } };
156
156
 
157
- return new Promise( (fulfill, reject) => {
158
- cdsFs( fileCache, options.traceFs ).readFile( filename, 'utf8', (err, source) => {
159
- if (err) {
160
- reject(err);
161
- }
162
- else {
163
- try {
164
- const ast = parseX( source, rel, options, model.$messageFunctions );
165
- a.sources[filename] = ast;
166
- ast.location = { file: rel };
167
- ast.dirname = path.dirname( filename );
168
- assertConsistency( ast, options );
169
- fulfill( ast );
170
- }
171
- catch (e) {
172
- reject( e );
173
- }
174
- }
175
- });
176
- });
157
+ const source = await cdsFs( fileCache, options.traceFs ).readFileAsync( filename, 'utf8' );
158
+ const ast = parseX( source, rel, options, model.$messageFunctions );
159
+ sources[filename] = ast;
160
+ ast.location = { file: rel };
161
+ ast.dirname = path.dirname( filename );
162
+ assertConsistency( ast, options );
163
+
164
+ return ast;
177
165
  }
178
166
 
179
167
  // Combine the parse results (if there are not file IO errors)
@@ -52,6 +52,7 @@ const { kindProperties } = require('./base');
52
52
  const {
53
53
  pushLink,
54
54
  setLink,
55
+ annotationVal,
55
56
  augmentPath,
56
57
  splitIntoPath,
57
58
  linkToOrigin,
@@ -359,13 +360,13 @@ function resolve( model ) {
359
360
  while (struct.kind === 'element')
360
361
  struct = struct._parent;
361
362
  if (struct.kind === 'select') {
362
- message( 'ref-invalid-typeof', [ ref.location, user ],
363
+ message( 'type-unexpected-typeof', [ ref.location, user ],
363
364
  { keyword: 'type of', '#': struct.kind } );
364
365
  // we actually refer to an element in _combined; TODO: return null if
365
366
  // not configurable; would produce illegal CSN with sub queries in FROM
366
367
  }
367
368
  else if (struct !== user._main) {
368
- message( 'ref-invalid-typeof', [ ref.location, user ],
369
+ message( 'type-unexpected-typeof', [ ref.location, user ],
369
370
  { keyword: 'type of', '#': struct.kind } );
370
371
  return setProp( ref, '_artifact', null );
371
372
  }
@@ -413,6 +414,7 @@ function resolve( model ) {
413
414
  for (const view of resolveChain.reverse()) {
414
415
  if (view._status !== '_query' ) { // not already resolved
415
416
  setProp( view, '_status', '_query' );
417
+ // must be run in order “sub query in FROM first”:
416
418
  traverseQueryPost( view.query, null, populateQuery );
417
419
  if (view.elements$) // specified elements
418
420
  mergeSpecifiedElements( view );
@@ -719,35 +721,69 @@ function resolve( model ) {
719
721
  else if (exposed.length === 1) {
720
722
  target = exposed[0];
721
723
  }
722
- else {
723
- message( (elem !== assoc ? 'redirected-implicitly-ambiguous' : 'type-ambiguous-target'),
724
- [ (elem.value || elem.name).location, elem ],
724
+ else if (elem === assoc) {
725
+ // `assoc: Association to ModelEntity`: user-provided target is to be auto-redirected
726
+ warning( 'type-ambiguous-target',
727
+ [ elem.target.location, elem ],
725
728
  {
726
729
  target,
727
- service,
728
- art: definitionScope( target ),
730
+ // art: definitionScope( target ), - TODO extra debug info in message
729
731
  sorted_arts: exposed,
730
- '#': ( elemScope !== true ? 'std' : 'scoped' ),
731
732
  }, {
732
733
  // eslint-disable-next-line max-len
733
- std: 'Target $(TARGET) is exposed in service $(SERVICE) by multiple projections $(SORTED_ARTS) - no implicit redirection',
734
+ std: 'Replace target $(TARGET) by one of $(SORTED_ARTS); can\'t auto-redirect this association if multiple projections exist in this service',
734
735
  // eslint-disable-next-line max-len
735
- scoped: 'Target $(TARGET) is defined in scope $(ART) which exposed in service $(SERVICE) by multiple projections - no implicit redirection',
736
+ two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service',
736
737
  });
738
+ // continuation semantics: no auto-redirection
739
+ }
740
+ else {
741
+ // referred (and probably inferred) assoc (without a user-provided target at that place)
742
+ // HINT: consider bin/cdsv2m.js when changing the following message text
743
+ // No grouped and sub messages yet (TODO v3): mention at all target places with all assocs
744
+ const withAnno = annotationVal( exposed[0]['@cds.redirection.target'] );
745
+ for (const proj of exposed) {
746
+ // TODO: def-ambiguous-target (just v3, as the current is infamous and used in options),
747
+ message( 'redirected-implicitly-ambiguous',
748
+ [ weakLocation( proj.location ), proj ],
749
+ {
750
+ '#': withAnno && 'justOne',
751
+ target,
752
+ art: elem,
753
+ // art: definitionScope( target ), - TODO extra debug info in message
754
+ anno: 'cds.redirection.target',
755
+ sorted_arts: exposed,
756
+ }, {
757
+ // eslint-disable-next-line max-len
758
+ std: 'Add $(ANNO) to one of $(SORTED_ARTS) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
759
+ // eslint-disable-next-line max-len
760
+ two: 'Add $(ANNO) to either $(SORTED_ARTS) or $(SECOND) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
761
+ // eslint-disable-next-line max-len
762
+ justOne: 'Remove $(ANNO) from all but one of $(SORTED_ARTS) to have a unique redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
763
+ } );
764
+ }
737
765
  // continuation semantics: no implicit redirections
738
766
  }
739
767
  }
740
768
  if (elem.target) { // redirection for Association to / Composition of
741
769
  if (elem.target._artifact === target) // no change (due to no implicit redirection)
742
770
  return true;
771
+ const type = resolvePath( elem.type, 'type', elem ); // cds.Association or cds.Composition
743
772
  const origin = {
744
- kind: elem.kind,
773
+ kind: elem.kind, // necessary for rewrite, '$user-provided' would be best
745
774
  name: elem.name,
775
+ type: {
776
+ path: [ { id: type.name.absolute, location: elem.type.location } ],
777
+ scope: 'global',
778
+ location: elem.type.location,
779
+ },
746
780
  target: elem.target,
747
781
  $inferred: 'REDIRECTED',
748
782
  location: elem.target.location,
749
783
  };
750
784
  setLink( elem, origin, '_origin' );
785
+ setLink( elem.type, type, '_artifact' );
786
+ setLink( origin, elem, '_outer' );
751
787
  setLink( origin, elem._parent, '_parent' );
752
788
  if (elem._main) // remark: the param `elem` can also be a type
753
789
  setLink( origin, elem._main, '_main' );
@@ -755,11 +791,11 @@ function resolve( model ) {
755
791
  setLink( origin, elem._block, '_block' );
756
792
  if (elem.foreignKeys) {
757
793
  origin.foreignKeys = elem.foreignKeys;
758
- delete elem.foreignKeys;
794
+ delete elem.foreignKeys; // will be rewritten
759
795
  }
760
796
  if (elem.on) {
761
797
  origin.on = elem.on;
762
- delete elem.on;
798
+ delete elem.on; // will be rewritten
763
799
  }
764
800
  }
765
801
  elem.target = {
@@ -818,10 +854,7 @@ function resolve( model ) {
818
854
  target._descendants[service.name.absolute] ||
819
855
  [],
820
856
  elemScope, target );
821
- const preferred = descendants.filter( ( d ) => {
822
- const anno = d['@cds.redirection.target'];
823
- return anno && (anno.val === undefined || anno.val );
824
- } );
857
+ const preferred = descendants.filter( d => annotationVal( d['@cds.redirection.target'] ) );
825
858
  const exposed = preferred.length ? preferred : descendants;
826
859
  if (exposed.length < 2)
827
860
  return exposed || [];
@@ -860,7 +893,7 @@ function resolve( model ) {
860
893
  // Need to filter out auto-exposed, otherwise the behavior is
861
894
  // processing-order dependent (not storing the autoexposed in
862
895
  // _descendents would only be an alternative w/o recompilation)
863
- return descendants.filter( d => !d['@cds.autoexposed'] );
896
+ return descendants.filter( d => !annotationVal( d['@cds.autoexposed'] ) );
864
897
  }
865
898
  // try scope as target first, even if it has @cds.redirection.target: false
866
899
  if (isDirectProjection( elemScope, target ))
@@ -991,6 +1024,7 @@ function resolve( model ) {
991
1024
  $inferred: 'autoexposed',
992
1025
  '@cds.autoexposed': {
993
1026
  name: { path: [ { id: 'cds.autoexposed', location } ], location },
1027
+ $inferred: 'autoexposed',
994
1028
  },
995
1029
  };
996
1030
  // TODO: do we need to tag the generated entity with elemScope = 'auto'?
@@ -2059,6 +2093,7 @@ function resolve( model ) {
2059
2093
  dependsOnSilent(art, key);
2060
2094
  }
2061
2095
  });
2096
+ obj.foreignKeys[$inferred] = 'keys';
2062
2097
  }
2063
2098
 
2064
2099
  function addForeignKeyNavigations( art ) {
@@ -2227,8 +2262,6 @@ function resolve( model ) {
2227
2262
  // Only top-level queries and sub queries in FROM
2228
2263
 
2229
2264
  function rewriteSimple( art ) {
2230
- // If we have a proper seperation of view elements and elements of the
2231
- // primary query, we can delete this function.
2232
2265
  // return;
2233
2266
  if (!art.includes && !art.query) {
2234
2267
  // console.log(message( null, art.location, art, {target:art._target},
@@ -2241,7 +2274,7 @@ function resolve( model ) {
2241
2274
  }
2242
2275
 
2243
2276
  function rewriteView( view ) {
2244
- traverseQueryPost( view.query, false, ( query ) => {
2277
+ traverseQueryExtra( view, ( query ) => {
2245
2278
  forEachGeneric( query, 'elements', rewriteAssociation );
2246
2279
  } );
2247
2280
  if (view.includes) // entities with structure includes:
@@ -2249,6 +2282,7 @@ function resolve( model ) {
2249
2282
  }
2250
2283
 
2251
2284
  // Check explicit ON / keys with REDIRECTED TO
2285
+ // TODO: run on all queries, but this is potentially incompatible
2252
2286
  function rewriteViewCheck( view ) {
2253
2287
  traverseQueryPost( view.query, false, ( query ) => {
2254
2288
  forEachGeneric( query, 'elements', rewriteAssociationCheck );
@@ -2474,7 +2508,7 @@ function resolve( model ) {
2474
2508
  resolveExpr( cond, rewriteExpr, elem, nav.tableAlias );
2475
2509
  }
2476
2510
  else {
2477
- // TODO: support that
2511
+ // TODO: support that, now that the ON condition is rewritten in the right order
2478
2512
  error( null, [ elem.value.location, elem ],
2479
2513
  'Selecting unmanaged associations from a sub query is not supported' );
2480
2514
  }
@@ -2880,8 +2914,10 @@ function navProjection( navigation, preferred ) {
2880
2914
  : navigation._projections[0] || null;
2881
2915
  }
2882
2916
 
2883
- // Query tree post-order traversal - called for everything which makes a query
2917
+ // Query tree post-order traversal - called for everything which contributes to the query
2918
+ // i.e. is necessary to calculate the elements of the query
2884
2919
  // except "real ones": operands of UNION etc, JOIN with ON, and sub queries in FROM
2920
+ // NOTE: does not run on non-referred sub queries! Consider using ‹main›.$queries instead!
2885
2921
  function traverseQueryPost( query, simpleOnly, callback ) {
2886
2922
  if (!query) // parser error
2887
2923
  return;
@@ -2919,4 +2955,34 @@ function traverseQueryPost( query, simpleOnly, callback ) {
2919
2955
  // else: with parse error (`select from <EOF>`, `select distinct from;`)
2920
2956
  }
2921
2957
 
2958
+ // Call callback on all queries in dependency order, i.e. starting with query Q
2959
+ // 1. sub queries in FROM sources of Q
2960
+ // 2. Q itself, except if non-referred query, but with right UNION parts
2961
+ // 3. sub queries in ON in FROM of Q
2962
+ // 4. sub queries in columns, WHERE, HAVING
2963
+ function traverseQueryExtra( main, callback ) {
2964
+ if (!main.$queries)
2965
+ return;
2966
+ // with a top-level UNION, $queries[0] is just the left
2967
+ traverseQueryPost( main.query, false, (q) => { // also with right of UNION (to be compatible)
2968
+ setProp( q, '_status', 'extra' );
2969
+ callback( q );
2970
+ } );
2971
+ for (const query of main.$queries.slice(1)) {
2972
+ if (query._status === 'extra' || query._parent.kind === '$tableAlias')
2973
+ continue; // if parent is alias, query is FROM source -> run by traverseQueryPost
2974
+ // we are now in the top-level (parent is entity) or a non-referred query (parent is query)
2975
+ setProp( query, '_status', 'extra' ); // do not call callback() in non-referred query
2976
+ // console.log( 'A:', query.name,query._status)
2977
+ traverseQueryPost( query, null, (q) => {
2978
+ if (q._status !== 'extra') {
2979
+ // console.log( 'T:', q.name)
2980
+ setProp( q, '_status', 'extra' );
2981
+ callback( q );
2982
+ }
2983
+ // else console.log( 'E:', q.name)
2984
+ } );
2985
+ }
2986
+ }
2987
+
2922
2988
  module.exports = resolve;
@@ -7,7 +7,7 @@ const { searchName } = require('../base/messages');
7
7
  const { dictAddArray } = require('../base/dictionaries');
8
8
  const { setProp } = require('../base/model');
9
9
 
10
- const { setLink, dependsOn } = require('./utils');
10
+ const { setLink, dependsOn, pathName } = require('./utils');
11
11
 
12
12
  function artifactsEnv( art ) {
13
13
  return art._subArtifacts || Object.create(null);
@@ -611,29 +611,24 @@ function fns( model ) {
611
611
 
612
612
  const fn = (spec.envFn && artItemsCount >= 0) ? spec.envFn : VolatileFns.environment;
613
613
  const env = fn( art, item.location, user, spec.assoc );
614
-
615
- // do not check any elements of the path, e.g. $session - but still don't return path-head
616
- if (art && art.$uncheckedElements) {
617
- if (env && env[item.id]) // something like $user.id/$user.locale
618
- return env[item.id];
619
-
620
- // $user.foo - build our own valid path step obj
621
- // Important: Don't directly modify item!
622
- const obj = {
623
- location: item.location,
624
- kind: 'builtin',
625
- name: { id: item.id, element: path.map(p => p.id).join('.') },
626
- };
627
- setLink(obj, art, '_parent');
628
- return obj;
629
- }
630
-
631
614
  const sub = setLink( item, env && env[item.id] );
632
615
 
633
- if (!sub)
634
- return (sub === 0) ? 0 : errorNotFound( item, env );
635
- else if (Array.isArray(sub)) // redefinitions
616
+ if (!sub) {
617
+ // element was not found in environment
618
+ if (sub === 0)
619
+ return 0;
620
+ if (art.$uncheckedElements) { // magic variable / replacement variable
621
+ signalNotFound( 'ref-unknown-var', [ item.location, user ], [ env ],
622
+ { id: path.map(n => n.id).join('.') } );
623
+ }
624
+ else {
625
+ errorNotFound( item, env );
626
+ }
627
+ return null;
628
+ }
629
+ else if (Array.isArray(sub)) { // redefinitions
636
630
  return false;
631
+ }
637
632
 
638
633
  if (nav) { // we have already "pseudo-followed" a managed association
639
634
  // We currently rely on the check that targetElement references do
@@ -715,7 +710,7 @@ function fns( model ) {
715
710
  { param: 'Entity $(ART) has no parameter $(MEMBER)' } );
716
711
  }
717
712
  else {
718
- signalNotFound( 'ref-undefined-element', [ item.location, user ],
713
+ signalNotFound( spec.undefinedDef || 'ref-undefined-element', [ item.location, user ],
719
714
  [ env ], { art: searchName( art, item.id, 'element' ) } );
720
715
  }
721
716
  return null;
@@ -814,7 +809,7 @@ function fns( model ) {
814
809
  setProp( a, '_block', block );
815
810
  a.$priority = priority;
816
811
  if (construct !== art)
817
- dictAddArray( art, annoProp, a );
812
+ addAnnotation( art, annoProp, a );
818
813
  }
819
814
  }
820
815
  }
@@ -865,15 +860,17 @@ function fns( model ) {
865
860
  // TODO: _parent, _main is set later (if we have ElementRef), or do we
866
861
  // set _artifact?
867
862
  anno.$priority = priority;
868
- dictAddArray( art, annoProp, anno );
863
+ addAnnotation( art, annoProp, anno );
869
864
  }
870
865
  }
871
866
  }
872
867
 
873
- // Return string 'A.B.C' for parsed source `A.B.C` (is vector of ids with
874
- // locations):
875
- function pathName(path) {
876
- return (path.broken) ? '' : path.map( id => id.id ).join('.');
868
+ // Add annotation to definition - overwriting $inferred annos
869
+ function addAnnotation( art, annoProp, anno ) {
870
+ const old = art[annoProp];
871
+ if (old && old.$inferred)
872
+ delete art[annoProp];
873
+ dictAddArray( art, annoProp, anno );
877
874
  }
878
875
 
879
876
  module.exports = {
@@ -181,6 +181,16 @@ function withAssociation( ref, test = testFunctionPlaceholder, alsoTestLast = fa
181
181
  return false;
182
182
  }
183
183
 
184
+ /**
185
+ * Return string 'A.B.C' for parsed source `A.B.C` (is vector of ids with
186
+ * locations).
187
+ *
188
+ * @param {XSN.Path} path
189
+ */
190
+ function pathName(path) {
191
+ return (path.broken) ? '' : path.map( id => id.id ).join('.');
192
+ }
193
+
184
194
  /**
185
195
  * Generates an XSN path out of the given name. Path segments are delimited by a dot.
186
196
  * Each segment will have the given location assigned.
@@ -215,6 +225,7 @@ module.exports = {
215
225
  setMemberParent,
216
226
  storeExtension,
217
227
  withAssociation,
228
+ pathName,
218
229
  augmentPath,
219
230
  splitIntoPath,
220
231
  };
@@ -1 +1 @@
1
- e80280336eed20b6633835d33df8ec76
1
+ cde5224056abde61cc2eb0b1b30bf694
@@ -37,6 +37,14 @@ let dictionaryPrototype = null;
37
37
  // stored with symbols as keys, as we do not want to disallow any key name:
38
38
  const $inferred = Symbol.for('cds.$inferred');
39
39
 
40
+ // XSN $inferred values mapped to Universal CSN $generated values:
41
+ const inferredAsGenerated = {
42
+ autoexposed: 'exposed',
43
+ 'localized-entity': 'localized',
44
+ localized: 'localized', // on elements (texts, localized)
45
+ 'composition-entity': 'composed', // ('aspect-composition' on element not in CSN)
46
+ };
47
+
40
48
  // IMPORTANT: the order of these properties determine the order of properties
41
49
  // in the resulting CSN !!! Also check const `csnPropertyNames`.
42
50
  const transformers = {
@@ -66,7 +74,7 @@ const transformers = {
66
74
  precision: value,
67
75
  scale: value,
68
76
  srid: value,
69
- cardinality: standard, // also for pathItem: after 'id', before 'where'
77
+ cardinality, // also in pathItem: after 'id', before 'where'
70
78
  targetAspect,
71
79
  target,
72
80
  foreignKeys,
@@ -609,10 +617,21 @@ function set( prop, csn, node ) {
609
617
  }
610
618
 
611
619
  function targetAspect( val, csn, node ) {
620
+ if (universalCsn) {
621
+ if (val.$inferred)
622
+ return undefined;
623
+ if (node.target) {
624
+ csn.$origin = { type: 'cds.Composition' };
625
+ if (node.cardinality)
626
+ csn.$origin.cardinality = standard( node.cardinality );
627
+ csn.$origin.target = (val.elements) ? standard( val ) : artifactRef( val, true );
628
+ return undefined;
629
+ }
630
+ }
612
631
  const ta = (val.elements)
613
632
  ? addLocation( val.location, standard( val ) )
614
633
  : artifactRef( val, true );
615
- if (!gensrcFlavor || node.target && !node.target.$inferred)
634
+ if (!gensrcFlavor && !universalCsn || node.target && !node.target.$inferred)
616
635
  return ta;
617
636
  // For compatibility, put aspect in 'target' with parse.cdl and csn flavor 'gensrc'
618
637
  csn.target = ta;
@@ -662,12 +681,7 @@ function enumDict( dict, csn, node ) {
662
681
  }
663
682
 
664
683
  function enumerableQueryElements( select ) {
665
- if (!universalCsn || select === select._main._leadingQuery)
666
- return false;
667
- if (select.orderBy || select.$orderBy)
668
- return true;
669
- const alias = select._parent;
670
- return alias.query && (alias.query._leadingQuery || alias.query) === select;
684
+ return (universalCsn && select !== select._main._leadingQuery);
671
685
  }
672
686
 
673
687
  // Should we render the elements? (and items?)
@@ -746,7 +760,8 @@ function ignore() { /* no-op: ignore property */ }
746
760
 
747
761
  function location( loc, csn, xsn ) {
748
762
  if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' &&
749
- (!xsn.$inferred || !xsn._main)) { // TODO: also for 'select'
763
+ (!xsn.$inferred || !xsn._main) &&
764
+ xsn.$inferred !== 'REDIRECTED') { // TODO: also for 'select'
750
765
  // Also include $location for elements in queries (if not via '*')
751
766
  addLocation( xsn.name && xsn.name.location || loc, csn );
752
767
  }
@@ -802,8 +817,10 @@ function dictionary( dict, keys, prop ) {
802
817
  }
803
818
 
804
819
  function foreignKeys( dict, csn, node ) {
805
- if (universalCsn && !target( node.target, csn, node ))
806
- return;
820
+ if (universalCsn) {
821
+ if (!target( node.target, csn, node ) || dict[$inferred] === 'keys')
822
+ return;
823
+ }
807
824
  if (gensrcFlavor && node._origin && node._origin.$inferred === 'REDIRECTED')
808
825
  dict = node._origin.foreignKeys;
809
826
  const keys = [];
@@ -836,8 +853,8 @@ function definition( art, _csn, _node, prop ) {
836
853
  delete c.elements;
837
854
  c.returns = { elements: elems };
838
855
  }
839
- if (kind && kind !== 'key')
840
- addOrigin( c, art, art._origin );
856
+ // precondition already fulfilled: art.kind !== 'key'
857
+ addOrigin( c, art, art._origin );
841
858
  return c;
842
859
  }
843
860
 
@@ -979,6 +996,8 @@ function originRef( art, user ) {
979
996
  }
980
997
 
981
998
  function kind( k, csn, node ) {
999
+ if (node.$inferred === 'REDIRECTED')
1000
+ return;
982
1001
  if (k === 'annotate' || k === 'extend') {
983
1002
  // We just use `name.absolute` because it is very likely a "constructed"
984
1003
  // extensions. The CSN parser must produce name.path like for other refs.
@@ -987,20 +1006,47 @@ function kind( k, csn, node ) {
987
1006
  else if (k === 'extend')
988
1007
  csn.kind = k;
989
1008
  }
1009
+ else if (k === 'action' && node._main && universalCsn && node.$inferred) {
1010
+ // Universal CSN: do not mention kind: 'action' on expanded action
1011
+ }
990
1012
  else if (![
991
1013
  'element', 'key', 'param', 'enum', 'select', '$join',
992
1014
  '$tableAlias', 'annotation', 'mixin',
993
1015
  ].includes(k)) {
994
1016
  csn.kind = k;
995
1017
  }
1018
+ const generated = universalCsn && inferredAsGenerated[node.$inferred];
1019
+ if (typeof generated === 'string')
1020
+ csn.$generated = generated;
996
1021
  }
997
1022
 
998
1023
  function type( node, csn, xsn ) {
999
- if (universalCsn && node.$inferred && xsn._origin)
1024
+ if (!universalCsn)
1025
+ return artifactRef( node, !node.$extra );
1026
+ if (node.$inferred)
1027
+ return undefined;
1028
+ if (xsn._origin) {
1029
+ if (xsn._origin.$inferred === 'REDIRECTED') { // auto-redirected user-provided target
1030
+ csn.$origin = definition( xsn._origin );
1031
+ return undefined;
1032
+ }
1033
+ }
1034
+ else if ( xsn.targetAspect && xsn.target ) {
1035
+ // type moved to $origin: { type: … }
1000
1036
  return undefined;
1037
+ }
1001
1038
  return artifactRef( node, !node.$extra );
1002
1039
  }
1003
1040
 
1041
+ function cardinality( node, csn, xsn ) {
1042
+ if (!universalCsn)
1043
+ return standard( node );
1044
+ // cardinality might be moved to $origin: { cardinality: … }
1045
+ if (node.$inferred || xsn.targetAspect && !xsn.targetAspect.$inferred && xsn.target)
1046
+ return undefined;
1047
+ return standard( node );
1048
+ }
1049
+
1004
1050
  function artifactRef( node, terse ) {
1005
1051
  // When called as transformer function, a CSN node is provided as argument
1006
1052
  // for `terse`, i.e. it is usually truthy, except for FROM
@@ -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 );
@@ -320,11 +320,12 @@ function multiLineTokenLocation(token, val) {
320
320
  return undefined;
321
321
 
322
322
  // Count the number of newlines in the token.
323
- // TODO: I want to avoid a substring, that's why I don't use RegEx here
324
323
  const source = token.source[1].data;
325
324
  let newLineCount = 0;
326
325
  let lastNewlineIndex = token.start;
327
326
  for (let i = token.start; i < token.stop; i++) {
327
+ // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
328
+ // because ANTLR only uses LF for line break detection.
328
329
  if (source[i] === 10) { // ASCII code for '\n'
329
330
  newLineCount++;
330
331
  lastNewlineIndex = i;