@sap/cds-compiler 4.1.2 → 4.2.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 (74) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/cdsc.js +6 -3
  3. package/doc/CHANGELOG_BETA.md +5 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +2 -2
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +24 -24
  8. package/lib/base/message-registry.js +41 -6
  9. package/lib/base/messages.js +7 -0
  10. package/lib/base/model.js +38 -8
  11. package/lib/checks/elements.js +11 -10
  12. package/lib/checks/manyNavigations.js +33 -0
  13. package/lib/checks/onConditions.js +5 -2
  14. package/lib/checks/queryNoDbArtifacts.js +2 -3
  15. package/lib/checks/selectItems.js +4 -55
  16. package/lib/checks/utils.js +3 -2
  17. package/lib/checks/validator.js +3 -1
  18. package/lib/compiler/.eslintrc.json +2 -1
  19. package/lib/compiler/assert-consistency.js +27 -24
  20. package/lib/compiler/base.js +6 -2
  21. package/lib/compiler/builtins.js +34 -34
  22. package/lib/compiler/checks.js +179 -208
  23. package/lib/compiler/classes.js +2 -2
  24. package/lib/compiler/cycle-detector.js +6 -6
  25. package/lib/compiler/define.js +66 -45
  26. package/lib/compiler/extend.js +81 -72
  27. package/lib/compiler/finalize-parse-cdl.js +26 -26
  28. package/lib/compiler/generate.js +61 -45
  29. package/lib/compiler/index.js +47 -49
  30. package/lib/compiler/kick-start.js +8 -7
  31. package/lib/compiler/moduleLayers.js +1 -1
  32. package/lib/compiler/populate.js +42 -35
  33. package/lib/compiler/propagator.js +6 -6
  34. package/lib/compiler/resolve.js +170 -126
  35. package/lib/compiler/shared.js +122 -45
  36. package/lib/compiler/tweak-assocs.js +93 -40
  37. package/lib/compiler/utils.js +15 -12
  38. package/lib/edm/.eslintrc.json +40 -1
  39. package/lib/edm/annotations/genericTranslation.js +721 -707
  40. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  41. package/lib/edm/csn2edm.js +389 -378
  42. package/lib/edm/edm.js +678 -772
  43. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  44. package/lib/edm/edmInboundChecks.js +29 -27
  45. package/lib/edm/edmPreprocessor.js +686 -646
  46. package/lib/edm/edmUtils.js +277 -296
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +1 -1
  49. package/lib/gen/languageParser.js +1253 -1276
  50. package/lib/json/from-csn.js +34 -4
  51. package/lib/json/to-csn.js +4 -4
  52. package/lib/language/language.g4 +2 -5
  53. package/lib/main.d.ts +61 -1
  54. package/lib/model/csnUtils.js +31 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +37 -2
  57. package/lib/modelCompare/utils/filter.js +1 -1
  58. package/lib/optionProcessor.js +15 -3
  59. package/lib/render/toCdl.js +30 -4
  60. package/lib/render/toSql.js +5 -9
  61. package/lib/render/utils/common.js +8 -6
  62. package/lib/transform/db/applyTransformations.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/constraints.js +47 -17
  65. package/lib/transform/db/expansion.js +133 -50
  66. package/lib/transform/db/flattening.js +75 -7
  67. package/lib/transform/forOdata.js +4 -1
  68. package/lib/transform/forRelationalDB.js +80 -62
  69. package/lib/transform/localized.js +91 -54
  70. package/lib/transform/transformUtils.js +9 -10
  71. package/lib/utils/file.js +7 -7
  72. package/lib/utils/moduleResolve.js +210 -121
  73. package/lib/utils/objectUtils.js +1 -1
  74. package/package.json +5 -5
@@ -18,6 +18,7 @@ const {
18
18
  isDirectComposition,
19
19
  copyExpr,
20
20
  } = require('./utils');
21
+ const { weakLocation } = require('../base/messages');
21
22
 
22
23
  function generate( model ) {
23
24
  const { options } = model;
@@ -33,7 +34,7 @@ function generate( model ) {
33
34
  applyIncludes,
34
35
  } = model.$functions;
35
36
 
36
- const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
37
+ const addTextsLanguageAssoc = checkTextsLanguageAssocOption( model, options );
37
38
  const useTextsAspect = checkTextsAspect();
38
39
 
39
40
  Object.keys( model.definitions ).forEach( processArtifact );
@@ -63,14 +64,14 @@ function generate( model ) {
63
64
  */
64
65
  function compositionChildPersistence() {
65
66
  const processed = new WeakSet();
66
- forEachDefinition(model, processCompositionPersistence);
67
+ forEachDefinition( model, processCompositionPersistence );
67
68
 
68
69
  function processCompositionPersistence( def ) {
69
- if (def.$inferred === 'composition-entity' && !processed.has(def)) {
70
+ if (def.$inferred === 'composition-entity' && !processed.has( def )) {
70
71
  if (def._parent)
71
- processCompositionPersistence(def._parent);
72
+ processCompositionPersistence( def._parent );
72
73
  copyPersistenceAnnotations( def, def._parent );
73
- processed.add(def);
74
+ processed.add( def );
74
75
  }
75
76
  }
76
77
  }
@@ -90,18 +91,18 @@ function generate( model ) {
90
91
  const specialElements = { locale: { key: true } };
91
92
 
92
93
  if (textsAspect.kind !== 'aspect' || !textsAspect.elements) {
93
- error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
94
- { '#': 'no-aspect', art: textsAspect });
94
+ error( 'def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
95
+ { '#': 'no-aspect', art: textsAspect } );
95
96
  return false;
96
97
  }
97
98
 
98
99
  let hasError = false;
99
100
  if (addTextsLanguageAssoc && textsAspect.elements.language) {
100
101
  const lang = textsAspect.elements.language;
101
- error('def-unexpected-element', [ lang.name.location, lang ],
102
- { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
103
- // eslint-disable-next-line max-len
104
- '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
102
+ error( 'def-unexpected-element', [ lang.name.location, lang ],
103
+ { option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
104
+ // eslint-disable-next-line max-len
105
+ '$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
105
106
  hasError = true;
106
107
  }
107
108
 
@@ -109,14 +110,14 @@ function generate( model ) {
109
110
  const expected = specialElements[name];
110
111
  const elem = textsAspect.elements[name];
111
112
  if (!elem) {
112
- error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
113
- { '#': 'missing', art: textsAspect, name });
113
+ error( 'def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
114
+ { '#': 'missing', art: textsAspect, name } );
114
115
  hasError = true;
115
116
  }
116
117
  else if (expected.key !== undefined && !!elem.key?.val !== expected.key) {
117
118
  const loc = elem.key?.location || elem.name?.location || textsAspect.name.location;
118
- error('def-invalid-texts-aspect', [ loc, elem ],
119
- { '#': expected.key ? 'key' : 'no-key', art: elem });
119
+ error( 'def-invalid-texts-aspect', [ loc, elem ],
120
+ { '#': expected.key ? 'key' : 'no-key', art: elem } );
120
121
  hasError = true;
121
122
  }
122
123
  }
@@ -193,9 +194,9 @@ function generate( model ) {
193
194
  // usual include-mechanism.
194
195
  const protectedElements = [ 'locale', 'texts', 'localized' ];
195
196
  if (fioriEnabled)
196
- protectedElements.push('ID_texts');
197
+ protectedElements.push( 'ID_texts' );
197
198
  if (addTextsLanguageAssoc)
198
- protectedElements.push('language');
199
+ protectedElements.push( 'language' );
199
200
 
200
201
  for (const name in art.elements) {
201
202
  const elem = art.elements[name];
@@ -214,6 +215,13 @@ function generate( model ) {
214
215
  else if (isLocalized) {
215
216
  textElems.push( elem );
216
217
  }
218
+
219
+ if (isKey && isLocalized) {
220
+ const errpos = elem.localized || elem.type || elem.name;
221
+ warning( 'def-ignoring-localized', [ errpos.location, elem ],
222
+ { keyword: 'localized' },
223
+ 'Keyword $(KEYWORD) is ignored for primary keys' );
224
+ }
217
225
  }
218
226
  if (textElems.length <= keys)
219
227
  return false;
@@ -336,8 +344,8 @@ function generate( model ) {
336
344
  // Because ID_texts is not copied from TextsAspect, the order is messed
337
345
  // up. Fix it.
338
346
  const { elements } = art;
339
- art.elements = Object.create(null);
340
- const names = [ 'ID_texts', 'locale', ...Object.keys(elements) ];
347
+ art.elements = Object.create( null );
348
+ const names = [ 'ID_texts', 'locale', ...Object.keys( elements ) ];
341
349
  for (const name of names)
342
350
  art.elements[name] = elements[name];
343
351
 
@@ -367,7 +375,7 @@ function generate( model ) {
367
375
  function createTextsEntityWithInclude( base, absolute, fioriEnabled ) {
368
376
  const textsAspectName = 'sap.common.TextsAspect';
369
377
  const textsAspect = model.definitions['sap.common.TextsAspect'];
370
- const elements = Object.create(null);
378
+ const elements = Object.create( null );
371
379
  const { location } = base.name;
372
380
  const art = {
373
381
  kind: 'entity',
@@ -415,7 +423,7 @@ function generate( model ) {
415
423
  * @param {boolean} fioriEnabled
416
424
  */
417
425
  function createTextsEntityWithDefaultElements( base, absolute, fioriEnabled ) {
418
- const elements = Object.create(null);
426
+ const elements = Object.create( null );
419
427
  const { location } = base.name;
420
428
  const art = {
421
429
  kind: 'entity',
@@ -528,7 +536,7 @@ function generate( model ) {
528
536
  * @returns {boolean}
529
537
  */
530
538
  function hasTruthyProp( art, prop ) {
531
- const processed = Object.create(null); // avoid infloops with circular refs
539
+ const processed = Object.create( null ); // avoid infloops with circular refs
532
540
  let name = art.name.absolute; // is ok, since no recursive type possible
533
541
  while (art && !processed[name]) {
534
542
  if (art[prop])
@@ -568,7 +576,7 @@ function generate( model ) {
568
576
  return;
569
577
 
570
578
  function baseKeys() {
571
- const k = Object.create(null);
579
+ const k = Object.create( null );
572
580
  for (const name in base.elements) {
573
581
  const elem = base.elements[name];
574
582
  if (elem.$duplicates)
@@ -654,22 +662,22 @@ function generate( model ) {
654
662
  return false;
655
663
  }
656
664
  const names = Object.keys( target.elements )
657
- .filter( n => n.startsWith('up__') && keyNames.includes( n.substring(4) ) );
665
+ .filter( n => n.startsWith( 'up__' ) && keyNames.includes( n.substring(4) ) );
658
666
  if (names.length) {
659
667
  // FUTURE: if named type, add sub info with location of "up_" element
660
668
  error( null, [ location, elem ], { target: entityName, names }, {
661
669
  std: 'Key elements $(NAMES) can\'t be added to $(TARGET) as these already exist',
662
670
  one: 'Key element $(NAMES) can\'t be added to $(TARGET) as it already exist',
663
- });
671
+ } );
664
672
  return false;
665
673
  }
666
674
 
667
- if (elem.type && !isDirectComposition(elem)) {
675
+ if (elem.type && !isDirectComposition( elem )) {
668
676
  // Only issue warning for direct usages, not for projections, includes, etc.
669
677
  // TODO: Make it configurable error; v4: error
670
678
  warning( 'def-expected-comp-aspect', [ elem.type.location, elem ],
671
679
  { prop: 'Composition of', otherprop: 'Association to' },
672
- 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect');
680
+ 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect' );
673
681
  }
674
682
 
675
683
  return true;
@@ -689,9 +697,14 @@ function generate( model ) {
689
697
 
690
698
  const art = {
691
699
  kind: 'entity',
692
- name: { path: splitIntoPath( location, entityName ), absolute: entityName, location },
700
+ name: {
701
+ path: splitIntoPath( location, entityName ),
702
+ absolute: entityName,
703
+ // for code navigation (e.g. via `extend`s): point to the element's name
704
+ location: elem.name.location,
705
+ },
693
706
  location,
694
- elements: Object.create(null),
707
+ elements: Object.create( null ),
695
708
  $inferred: 'composition-entity',
696
709
  };
697
710
  if (target.name) { // named target aspect
@@ -704,17 +717,19 @@ function generate( model ) {
704
717
  setLink( art, '_upperAspects', elem._main._upperAspects || [] );
705
718
  }
706
719
 
720
+ // Since there is no user-written up_ element, use a weak location to the beginning of {…}.
721
+ const upLocation = weakLocation( location );
707
722
  const up = { // elements.up_ = ...
708
- name: { location, id: 'up_' },
723
+ name: { location: upLocation, id: 'up_' },
709
724
  kind: 'element',
710
- location,
725
+ location: upLocation,
711
726
  $inferred: 'aspect-composition',
712
- type: augmentPath( location, 'cds.Association' ),
713
- target: augmentPath( location, base.name.absolute ),
727
+ type: augmentPath( upLocation, 'cds.Association' ),
728
+ target: augmentPath( upLocation, base.name.absolute ),
714
729
  cardinality: {
715
- targetMin: { val: 1, literal: 'number', location },
716
- targetMax: { val: 1, literal: 'number', location },
717
- location,
730
+ targetMin: { val: 1, literal: 'number', location: upLocation },
731
+ targetMax: { val: 1, literal: 'number', location: upLocation },
732
+ location: upLocation,
718
733
  },
719
734
  };
720
735
  // By default, 'up_' is a managed primary key association.
@@ -723,16 +738,17 @@ function generate( model ) {
723
738
  if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) {
724
739
  addProxyElements( art, keys, 'aspect-composition', target.name && location,
725
740
  'up__', '@odata.containment.ignore' );
726
- up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
741
+ up.on = augmentEqual( upLocation, 'up_', Object.values( keys ), 'up__' );
727
742
  }
728
743
  else {
729
- up.key = { location, val: true };
744
+ up.key = { location: upLocation, val: true };
730
745
  // managed associations must be explicitly set to not null
731
746
  // even if target cardinality is 1..1
732
- up.notNull = { location, val: true };
747
+ up.notNull = { location: upLocation, val: true };
733
748
  }
734
749
 
735
- dictAdd( art.elements, 'up_', up);
750
+ dictAdd( art.elements, 'up_', up );
751
+ // Only for named aspects, use a new location; otherwise use the origin's one.
736
752
  addProxyElements( art, target.elements, 'aspect-composition', target.name && location );
737
753
 
738
754
  setLink( art, '_block', model.$internal );
@@ -750,7 +766,7 @@ function generate( model ) {
750
766
  for (const name in elements) {
751
767
  const pname = `${ prefix }${ name }`;
752
768
  const origin = elements[name];
753
- const proxy = linkToOrigin( origin, pname, null, null, location || origin.location );
769
+ const proxy = linkToOrigin( origin, pname, null, null, location, true );
754
770
  setLink( proxy, '_block', origin._block );
755
771
  proxy.$inferred = inferred;
756
772
  if (origin.masked)
@@ -801,7 +817,7 @@ function augmentEqual( location, assocname, relations, prefix = '' ) {
801
817
  : { op: { val: 'and', location }, args, location };
802
818
 
803
819
  function eq( refs ) {
804
- if (Array.isArray(refs))
820
+ if (Array.isArray( refs ))
805
821
  return { op: { val: '=', location }, args: refs.map( ref ), location };
806
822
 
807
823
  const { id } = refs.name;
@@ -815,7 +831,7 @@ function augmentEqual( location, assocname, relations, prefix = '' ) {
815
831
  };
816
832
  }
817
833
  function ref( path ) {
818
- return { path: path.split('.').map( id => ({ id, location }) ), location };
834
+ return { path: path.split( '.' ).map( id => ({ id, location }) ), location };
819
835
  }
820
836
  }
821
837
 
@@ -826,12 +842,12 @@ function checkTextsLanguageAssocOption( model, options ) {
826
842
  if (options.addTextsLanguageAssoc && !commonLanguagesEntity) {
827
843
  const variant = !languages ? 'std' : 'code';
828
844
  const loc = model.definitions['sap.common.Languages']?.name?.location || null;
829
- model.$messageFunctions.info('api-ignoring-language-assoc', loc, {
845
+ model.$messageFunctions.info( 'api-ignoring-language-assoc', loc, {
830
846
  '#': variant, option: 'addTextsLanguageAssoc', art: 'sap.common.Languages', name: 'code',
831
847
  }, {
832
848
  std: 'Ignoring option $(OPTION) because entity $(ART) is missing',
833
849
  code: 'Ignoring option $(OPTION) because entity $(ART) is missing element $(NAME)',
834
- });
850
+ } );
835
851
  }
836
852
 
837
853
  return !!commonLanguagesEntity;
@@ -12,7 +12,7 @@
12
12
 
13
13
  'use strict';
14
14
 
15
- const { resolveModule, resolveModuleSync } = require('../utils/moduleResolve');
15
+ const { makeModuleResolver, makeModuleResolverSync } = require('../utils/moduleResolve');
16
16
  const parseLanguage = require('../language/antlrParser'); // TODO: should we do some lazyload here?
17
17
  const parseCsn = require('../json/from-csn');
18
18
 
@@ -53,7 +53,7 @@ const extensionParsers = {
53
53
  // `errors`: vector of errors (file IO or ArgumentError)
54
54
  class InvocationError extends Error {
55
55
  constructor( errs, ...args ) {
56
- super(...args);
56
+ super( ...args );
57
57
  this.code = 'ERR_CDS_COMPILER_INVOCATION';
58
58
  this.errors = errs;
59
59
  this.hasBeenReported = false;
@@ -64,7 +64,7 @@ class InvocationError extends Error {
64
64
  // `argument`: the command argument (repeated file names)
65
65
  class ArgumentError extends Error {
66
66
  constructor( arg, ...args ) {
67
- super(...args);
67
+ super( ...args );
68
68
  this.code = 'ERR_CDS_COMPILER_ARGUMENT';
69
69
  this.argument = arg;
70
70
  }
@@ -86,15 +86,15 @@ function parseX( source, filename, options = {}, messageFunctions = null ) {
86
86
  const ext = path.extname( filename ).slice(1).toLowerCase();
87
87
  // eslint-disable-next-line no-nested-ternary
88
88
  const parser = options.fallbackParser === 'auto!'
89
- ? (source?.startsWith('{') ? parseCsn.parse : parseLanguage)
89
+ ? (source?.startsWith( '{' ) ? parseCsn.parse : parseLanguage)
90
90
  : (extensionParsers[ext] || extensionParsers[options.fallbackParser] ||
91
- source.startsWith('{') && parseCsn.parse);
91
+ source.startsWith( '{' ) && parseCsn.parse);
92
92
  if (parser)
93
93
  return parser( source, filename, options, messageFunctions );
94
94
 
95
95
  const model = new XsnSource();
96
96
  model.location = new CsnLocation( filename );
97
- messageFunctions.error( 'file-unknown-ext', emptyWeakLocation(filename),
97
+ messageFunctions.error( 'file-unknown-ext', emptyWeakLocation( filename ),
98
98
  { file: ext, '#': !ext && 'none' }, {
99
99
  std: 'Unknown file extension $(FILE)',
100
100
  none: 'No file extension',
@@ -131,29 +131,30 @@ function parseX( source, filename, options = {}, messageFunctions = null ) {
131
131
  // - 'string' or instanceof Buffer: the file content
132
132
  // - { realname: fs.realpath(filename) }: if filename is not canonicalized
133
133
  //
134
- function compileX( filenames, dir = '', options = {}, fileCache = Object.create(null) ) {
134
+ function compileX( filenames, dir = '', options = {}, fileCache = Object.create( null ) ) {
135
135
  // A non-proper dictionary (i.e. with prototype) is safe if the keys are
136
136
  // absolute file names - they start with `/` or `\` or similar
137
137
  // if (Object.getPrototypeOf( fileCache ))
138
138
  // fileCache = Object.assign( Object.create(null), fileCache );
139
- dir = path.resolve(dir);
139
+ dir = path.resolve( dir );
140
140
  const model = { sources: null, options };
141
141
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
142
+ const { resolveModule } = makeModuleResolver( options, fileCache, model.$messageFunctions );
142
143
  let input = null;
143
144
 
144
145
  let all = processFilenames( filenames, dir )
145
- .then((processedInput) => {
146
+ .then( (processedInput) => {
146
147
  input = processedInput;
147
148
  model.sources = input.sources;
148
- })
149
- .then(() => promiseAllDoNotRejectImmediately( input.files.map(readAndParse) ))
149
+ } )
150
+ .then( () => promiseAllDoNotRejectImmediately( input.files.map( readAndParse ) ) )
150
151
  .then( testInvocation, (reason) => {
151
152
  // do not reject with PromiseAllError, use InvocationError:
152
153
  const errs = reason.valuesOrErrors.filter( e => e instanceof Error );
153
154
  // internal error if no file IO error (has property `path`)
154
155
  return Promise.reject( errs.find( e => !e.path ) ||
155
156
  new InvocationError( [ ...input.repeated, ...errs ]) );
156
- });
157
+ } );
157
158
 
158
159
  if (!options.parseOnly && !options.parseCdl)
159
160
  all = all.then( readDependencies );
@@ -161,7 +162,7 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
161
162
  return all.then( () => {
162
163
  moduleLayers.setLayers( input.sources );
163
164
  return compileDoX( model );
164
- });
165
+ } );
165
166
 
166
167
  // Read file `filename` and parse its content, return messages
167
168
  async function readAndParse( filename ) {
@@ -210,10 +211,8 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
210
211
  }
211
212
  // create promises after all usingFroms have been collected, as the
212
213
  // Promise executor is called immediately with `new`:
213
- for (const module in dependencies) {
214
- promises.push( resolveModule( dependencies[module], fileCache, options,
215
- model.$messageFunctions ) );
216
- }
214
+ for (const module in dependencies)
215
+ promises.push( resolveModule( dependencies[module] ) );
217
216
  }
218
217
  if (!promises.length)
219
218
  return [];
@@ -236,23 +235,25 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
236
235
  * @param {object} [fileCache]
237
236
  * @returns {XSN.Model} Augmented CSN
238
237
  */
239
- function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.create(null) ) {
238
+ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.create( null ) ) {
240
239
  // A non-proper dictionary (i.e. with prototype) is safe if the keys are
241
240
  // absolute file names - they start with `/` or `\` or similar
242
- dir = path.resolve(dir);
241
+ dir = path.resolve( dir );
243
242
  const a = processFilenamesSync( filenames, dir );
244
243
 
245
244
  const model = { sources: a.sources, options };
246
245
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
246
+ const { resolveModuleSync } = makeModuleResolverSync( options, fileCache,
247
+ model.$messageFunctions );
247
248
 
248
249
  const asts = [];
249
250
  const errors = [];
250
251
  a.files.forEach( val => readAndParseSync( val, (err, ast) => {
251
252
  if (err)
252
- errors.push(err);
253
+ errors.push( err );
253
254
  if (ast)
254
- asts.push(ast);
255
- }));
255
+ asts.push( ast );
256
+ } ) );
256
257
 
257
258
  if (errors.length || a.repeated.length) {
258
259
  // internal error if no file IO error (has property `path`)
@@ -266,12 +267,12 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
266
267
  asts.length = 0;
267
268
  // Push dependencies to `ast`. Only works because readAndParseSync() is synchronous.
268
269
  for (const fileName of fileNames) {
269
- readAndParseSync(fileName, ( err, ast ) => {
270
+ readAndParseSync( fileName, ( err, ast ) => {
270
271
  if (err)
271
272
  throw err;
272
273
  if (ast)
273
274
  asts.push( ast );
274
- });
275
+ } );
275
276
  }
276
277
  }
277
278
  }
@@ -282,12 +283,12 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
282
283
  // Read file `filename` and parse its content, return messages
283
284
  function readAndParseSync( filename, cb ) {
284
285
  if ( filename === false ) { // module which has not been found
285
- cb(null, null);
286
+ cb( null, null );
286
287
  return;
287
288
  }
288
289
  const rel = a.sources[filename] || path.relative( dir, filename );
289
290
  if (typeof rel === 'object') { // already parsed
290
- cb(null, null);
291
+ cb( null, null );
291
292
  return; // no further dependency processing
292
293
  }
293
294
  // no parallel readAndParse with same resolved filename should read the file,
@@ -296,7 +297,7 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
296
297
 
297
298
  cdsFs( fileCache, options.traceFs ).readFileSync( filename, 'utf8', (err, source) => {
298
299
  if (err) {
299
- cb(err, null);
300
+ cb( err, null );
300
301
  }
301
302
  else {
302
303
  try {
@@ -308,10 +309,10 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
308
309
  cb( null, ast );
309
310
  }
310
311
  catch (e) {
311
- cb(e, null);
312
+ cb( e, null );
312
313
  }
313
314
  }
314
- });
315
+ } );
315
316
  }
316
317
 
317
318
  function readDependenciesSync( astArray ) {
@@ -331,10 +332,8 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
331
332
  }
332
333
  // create promises after all usingFroms have been collected, as the
333
334
  // Promise executor is called immediately with `new`:
334
- for (const module in dependencies) {
335
- fileNames.push( resolveModuleSync( dependencies[module], fileCache, options,
336
- model.$messageFunctions ) );
337
- }
335
+ for (const module in dependencies)
336
+ fileNames.push( resolveModuleSync( dependencies[module] ) );
338
337
  }
339
338
  if (!fileNames.length)
340
339
  return [];
@@ -366,7 +365,7 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
366
365
  function compileSourcesX( sourcesDict, options = {} ) {
367
366
  if (typeof sourcesDict === 'string')
368
367
  sourcesDict = { '<stdin>.cds': sourcesDict };
369
- const sources = Object.create(null);
368
+ const sources = Object.create( null );
370
369
  const model = { sources, options };
371
370
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
372
371
 
@@ -424,9 +423,9 @@ function recompileX( csn, options ) {
424
423
  // TODO: $recompile: true should be enough
425
424
 
426
425
  const file = csn.$location && csn.$location.file &&
427
- csn.$location.file.replace(/[.]cds$/, '.cds.csn') || '<recompile>.csn';
426
+ csn.$location.file.replace( /[.]cds$/, '.cds.csn' ) || '<recompile>.csn';
428
427
 
429
- const sources = Object.create(null);
428
+ const sources = Object.create( null );
430
429
  const model = { sources, options };
431
430
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
432
431
  // TODO: or use module which invokes the recompilation?
@@ -435,7 +434,7 @@ function recompileX( csn, options ) {
435
434
  moduleLayers.setLayers( sources );
436
435
  const compiled = compileDoX( model ); // calls throwWithError()
437
436
  if (options.messages) // does not help with exception in compileDoX()
438
- deduplicateMessages(options.messages); // TODO: do better
437
+ deduplicateMessages( options.messages ); // TODO: do better
439
438
  return compiled;
440
439
  }
441
440
 
@@ -480,12 +479,11 @@ function compileDoX( model ) {
480
479
  resolve( model );
481
480
  tweakAssocs( model );
482
481
  assertConsistency( model );
482
+ check( model );
483
483
  throwWithError();
484
484
  if (options.lintMode)
485
485
  return model;
486
486
 
487
- check(model);
488
- throwWithError();
489
487
  return propagator.propagate( model );
490
488
  }
491
489
 
@@ -500,7 +498,7 @@ function compileDoX( model ) {
500
498
  * not normalized - any strings work
501
499
  */
502
500
  async function processFilenames( filenames, dir ) {
503
- const filenameMap = Object.create(null);
501
+ const filenameMap = Object.create( null );
504
502
 
505
503
  const promises = [];
506
504
  for (const originalName of filenames) {
@@ -510,12 +508,12 @@ async function processFilenames( filenames, dir ) {
510
508
  // Resolve possible symbolic link; if the file does not exist
511
509
  // we just continue using the original name because readFile()
512
510
  // already handles non-existent files.
513
- const promise = fs.promises.realpath(path.resolve(dir, originalName))
514
- .then(setName, () => setName(originalName));
515
- promises.push(promise);
511
+ const promise = fs.promises.realpath( path.resolve( dir, originalName ) )
512
+ .then( setName, () => setName( originalName ) );
513
+ promises.push( promise );
516
514
  }
517
515
 
518
- await Promise.all(promises);
516
+ await Promise.all( promises );
519
517
  return createSourcesDict( filenames, filenameMap, dir );
520
518
  }
521
519
 
@@ -523,15 +521,15 @@ async function processFilenames( filenames, dir ) {
523
521
  * Synchronous version of processFilenames().
524
522
  */
525
523
  function processFilenamesSync( filenames, dir ) {
526
- const filenameMap = Object.create(null);
524
+ const filenameMap = Object.create( null );
527
525
 
528
526
  for (const originalName of filenames) {
529
- let name = path.resolve(dir, originalName);
527
+ let name = path.resolve( dir, originalName );
530
528
  try {
531
529
  // Resolve possible symbolic link; if the file does not exist
532
530
  // we just continue using the original name because readFile()
533
531
  // already handles non-existent files.
534
- name = fs.realpathSync.native(name);
532
+ name = fs.realpathSync.native( name );
535
533
  }
536
534
  catch (e) {
537
535
  // Ignore the not-found (ENOENT) error
@@ -553,7 +551,7 @@ function processFilenamesSync( filenames, dir ) {
553
551
  * @return {{sources: object, files: string[], repeated: ArgumentError[]}}
554
552
  */
555
553
  function createSourcesDict( filenames, filenameMap, dir ) {
556
- const sources = Object.create(null);
554
+ const sources = Object.create( null );
557
555
  const files = [];
558
556
  const repeated = [];
559
557
 
@@ -561,7 +559,7 @@ function createSourcesDict( filenames, filenameMap, dir ) {
561
559
  const name = filenameMap[originalName];
562
560
  if (!sources[name]) {
563
561
  sources[name] = path.relative( dir, name );
564
- files.push(name);
562
+ files.push( name );
565
563
  }
566
564
  else if (typeof sources[name] === 'string') { // not specified more than twice
567
565
  const msg = `Repeated argument: file '${ sources[name] }'`;
@@ -33,7 +33,7 @@ function kickStart( model ) {
33
33
  const art = model.definitions[name];
34
34
  if (art._parent === undefined)
35
35
  return; // nothing to do for builtins and redefinitions
36
- if (art.query && art._ancestors === undefined)
36
+ if (art.query && art._ancestors === undefined && art.kind === 'entity')
37
37
  setProjectionAncestors( art );
38
38
 
39
39
  let parent = art._parent;
@@ -44,7 +44,7 @@ function kickStart( model ) {
44
44
  if (!parent || !service)
45
45
  return;
46
46
  // To be removed when nested services are allowed
47
- if (!isBetaEnabled(options, 'nestedServices') && art.kind === 'service') {
47
+ if (!isBetaEnabled( options, 'nestedServices' ) && art.kind === 'service') {
48
48
  while (parent.kind !== 'service')
49
49
  parent = parent._parent;
50
50
  message( 'service-nested-service', [ art.name.location, art ], { art: parent },
@@ -66,15 +66,16 @@ function kickStart( model ) {
66
66
  // TODO: do not do implicit redirection across services, i.e. Service2.E is
67
67
  // no redirection target for E if Service2.E = projection on Service1.E and
68
68
  // Service1.E = projection on E
69
+
70
+ // Remark: _ancestors are also set with includes, and there also for aspects,
71
+ // types and events.
69
72
  const chain = [];
70
73
  const autoexposed = annotationVal( art['@cds.autoexposed'] );
71
- const preferredRedirectionTarget = annotationVal( art['@cds.redirection.target'] );
72
74
  // no need to set preferredRedirectionTarget in the while loop as we would
73
75
  // use the projection having @cds.redirection.target anyhow instead of
74
76
  // `art` anyway (if we do the no-x-service-implicit-redirection TODO above)
75
77
  while (art?.query?.from?.path && // direct select with one source
76
- art._ancestors !== 0 && // prevent inf-loop
77
- (preferredRedirectionTarget || !annotationIsFalse( art['@cds.redirection.target'] ) )) {
78
+ art._ancestors !== 0) { // prevent inf-loop
78
79
  chain.push( art );
79
80
  setLink( art, '_ancestors', 0 ); // avoid infloop with cyclic from
80
81
  const name = resolveUncheckedPath( art.query.from, 'from', art );
@@ -109,10 +110,10 @@ function kickStart( model ) {
109
110
  return;
110
111
 
111
112
  function expose( ancestor ) {
112
- if (ancestor._service === service)
113
+ if (ancestor._service === service || annotationIsFalse( art['@cds.redirection.target'] ))
113
114
  return;
114
115
  const desc = ancestor._descendants ||
115
- setLink( ancestor, '_descendants', Object.create(null) );
116
+ setLink( ancestor, '_descendants', Object.create( null ) );
116
117
  if (!desc[sname])
117
118
  desc[sname] = [ art ];
118
119
  else
@@ -23,7 +23,7 @@ function setLayers( sources ) {
23
23
 
24
24
  // It is ensured that the representative is called last in SCC and that
25
25
  // dependent SCCs are called first
26
- function setExtends( node, representative, sccDeps = Object.create(null) ) {
26
+ function setExtends( node, representative, sccDeps = Object.create( null ) ) {
27
27
  setLink( node, '_layerRepresentative', representative );
28
28
  if (layerRepresentative !== representative) {
29
29
  layerRepresentative = representative;