@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
@@ -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];
@@ -215,9 +216,10 @@ function generate( model ) {
215
216
  textElems.push( elem );
216
217
  }
217
218
 
218
- if (isKey && isLocalized) { // key with localized is wrong - ignore localized
219
+ if (isKey && isLocalized) {
219
220
  const errpos = elem.localized || elem.type || elem.name;
220
- warning( 'def-ignoring-localized-key', [ errpos.location, elem ], { keyword: 'localized' },
221
+ warning( 'def-ignoring-localized', [ errpos.location, elem ],
222
+ { keyword: 'localized' },
221
223
  'Keyword $(KEYWORD) is ignored for primary keys' );
222
224
  }
223
225
  }
@@ -342,8 +344,8 @@ function generate( model ) {
342
344
  // Because ID_texts is not copied from TextsAspect, the order is messed
343
345
  // up. Fix it.
344
346
  const { elements } = art;
345
- art.elements = Object.create(null);
346
- const names = [ 'ID_texts', 'locale', ...Object.keys(elements) ];
347
+ art.elements = Object.create( null );
348
+ const names = [ 'ID_texts', 'locale', ...Object.keys( elements ) ];
347
349
  for (const name of names)
348
350
  art.elements[name] = elements[name];
349
351
 
@@ -373,7 +375,7 @@ function generate( model ) {
373
375
  function createTextsEntityWithInclude( base, absolute, fioriEnabled ) {
374
376
  const textsAspectName = 'sap.common.TextsAspect';
375
377
  const textsAspect = model.definitions['sap.common.TextsAspect'];
376
- const elements = Object.create(null);
378
+ const elements = Object.create( null );
377
379
  const { location } = base.name;
378
380
  const art = {
379
381
  kind: 'entity',
@@ -386,7 +388,6 @@ function generate( model ) {
386
388
 
387
389
  if (!fioriEnabled) {
388
390
  // To be compatible, we switch off draft without @fiori.draft.enabled
389
- // TODO (next major version): remove?
390
391
  setAnnotation( art, '@odata.draft.enabled', art.location, false );
391
392
  }
392
393
  else {
@@ -422,7 +423,7 @@ function generate( model ) {
422
423
  * @param {boolean} fioriEnabled
423
424
  */
424
425
  function createTextsEntityWithDefaultElements( base, absolute, fioriEnabled ) {
425
- const elements = Object.create(null);
426
+ const elements = Object.create( null );
426
427
  const { location } = base.name;
427
428
  const art = {
428
429
  kind: 'entity',
@@ -446,7 +447,6 @@ function generate( model ) {
446
447
  if (!fioriEnabled) {
447
448
  locale.key = { val: true, location };
448
449
  // To be compatible, we switch off draft without @fiori.draft.enabled
449
- // TODO (next major version): remove?
450
450
  setAnnotation( art, '@odata.draft.enabled', art.location, false );
451
451
  }
452
452
  else {
@@ -536,7 +536,7 @@ function generate( model ) {
536
536
  * @returns {boolean}
537
537
  */
538
538
  function hasTruthyProp( art, prop ) {
539
- const processed = Object.create(null); // avoid infloops with circular refs
539
+ const processed = Object.create( null ); // avoid infloops with circular refs
540
540
  let name = art.name.absolute; // is ok, since no recursive type possible
541
541
  while (art && !processed[name]) {
542
542
  if (art[prop])
@@ -576,7 +576,7 @@ function generate( model ) {
576
576
  return;
577
577
 
578
578
  function baseKeys() {
579
- const k = Object.create(null);
579
+ const k = Object.create( null );
580
580
  for (const name in base.elements) {
581
581
  const elem = base.elements[name];
582
582
  if (elem.$duplicates)
@@ -662,22 +662,22 @@ function generate( model ) {
662
662
  return false;
663
663
  }
664
664
  const names = Object.keys( target.elements )
665
- .filter( n => n.startsWith('up__') && keyNames.includes( n.substring(4) ) );
665
+ .filter( n => n.startsWith( 'up__' ) && keyNames.includes( n.substring(4) ) );
666
666
  if (names.length) {
667
667
  // FUTURE: if named type, add sub info with location of "up_" element
668
668
  error( null, [ location, elem ], { target: entityName, names }, {
669
669
  std: 'Key elements $(NAMES) can\'t be added to $(TARGET) as these already exist',
670
670
  one: 'Key element $(NAMES) can\'t be added to $(TARGET) as it already exist',
671
- });
671
+ } );
672
672
  return false;
673
673
  }
674
674
 
675
- if (elem.type && !isDirectComposition(elem)) {
675
+ if (elem.type && !isDirectComposition( elem )) {
676
676
  // Only issue warning for direct usages, not for projections, includes, etc.
677
677
  // TODO: Make it configurable error; v4: error
678
678
  warning( 'def-expected-comp-aspect', [ elem.type.location, elem ],
679
679
  { prop: 'Composition of', otherprop: 'Association to' },
680
- 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect');
680
+ 'Expected $(PROP), but found $(OTHERPROP) for composition of aspect' );
681
681
  }
682
682
 
683
683
  return true;
@@ -697,9 +697,14 @@ function generate( model ) {
697
697
 
698
698
  const art = {
699
699
  kind: 'entity',
700
- 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
+ },
701
706
  location,
702
- elements: Object.create(null),
707
+ elements: Object.create( null ),
703
708
  $inferred: 'composition-entity',
704
709
  };
705
710
  if (target.name) { // named target aspect
@@ -712,17 +717,19 @@ function generate( model ) {
712
717
  setLink( art, '_upperAspects', elem._main._upperAspects || [] );
713
718
  }
714
719
 
720
+ // Since there is no user-written up_ element, use a weak location to the beginning of {…}.
721
+ const upLocation = weakLocation( location );
715
722
  const up = { // elements.up_ = ...
716
- name: { location, id: 'up_' },
723
+ name: { location: upLocation, id: 'up_' },
717
724
  kind: 'element',
718
- location,
725
+ location: upLocation,
719
726
  $inferred: 'aspect-composition',
720
- type: augmentPath( location, 'cds.Association' ),
721
- target: augmentPath( location, base.name.absolute ),
727
+ type: augmentPath( upLocation, 'cds.Association' ),
728
+ target: augmentPath( upLocation, base.name.absolute ),
722
729
  cardinality: {
723
- targetMin: { val: 1, literal: 'number', location },
724
- targetMax: { val: 1, literal: 'number', location },
725
- location,
730
+ targetMin: { val: 1, literal: 'number', location: upLocation },
731
+ targetMax: { val: 1, literal: 'number', location: upLocation },
732
+ location: upLocation,
726
733
  },
727
734
  };
728
735
  // By default, 'up_' is a managed primary key association.
@@ -731,16 +738,17 @@ function generate( model ) {
731
738
  if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) {
732
739
  addProxyElements( art, keys, 'aspect-composition', target.name && location,
733
740
  'up__', '@odata.containment.ignore' );
734
- up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
741
+ up.on = augmentEqual( upLocation, 'up_', Object.values( keys ), 'up__' );
735
742
  }
736
743
  else {
737
- up.key = { location, val: true };
744
+ up.key = { location: upLocation, val: true };
738
745
  // managed associations must be explicitly set to not null
739
746
  // even if target cardinality is 1..1
740
- up.notNull = { location, val: true };
747
+ up.notNull = { location: upLocation, val: true };
741
748
  }
742
749
 
743
- 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.
744
752
  addProxyElements( art, target.elements, 'aspect-composition', target.name && location );
745
753
 
746
754
  setLink( art, '_block', model.$internal );
@@ -758,7 +766,7 @@ function generate( model ) {
758
766
  for (const name in elements) {
759
767
  const pname = `${ prefix }${ name }`;
760
768
  const origin = elements[name];
761
- const proxy = linkToOrigin( origin, pname, null, null, location || origin.location );
769
+ const proxy = linkToOrigin( origin, pname, null, null, location, true );
762
770
  setLink( proxy, '_block', origin._block );
763
771
  proxy.$inferred = inferred;
764
772
  if (origin.masked)
@@ -809,7 +817,7 @@ function augmentEqual( location, assocname, relations, prefix = '' ) {
809
817
  : { op: { val: 'and', location }, args, location };
810
818
 
811
819
  function eq( refs ) {
812
- if (Array.isArray(refs))
820
+ if (Array.isArray( refs ))
813
821
  return { op: { val: '=', location }, args: refs.map( ref ), location };
814
822
 
815
823
  const { id } = refs.name;
@@ -823,7 +831,7 @@ function augmentEqual( location, assocname, relations, prefix = '' ) {
823
831
  };
824
832
  }
825
833
  function ref( path ) {
826
- return { path: path.split('.').map( id => ({ id, location }) ), location };
834
+ return { path: path.split( '.' ).map( id => ({ id, location }) ), location };
827
835
  }
828
836
  }
829
837
 
@@ -834,12 +842,12 @@ function checkTextsLanguageAssocOption( model, options ) {
834
842
  if (options.addTextsLanguageAssoc && !commonLanguagesEntity) {
835
843
  const variant = !languages ? 'std' : 'code';
836
844
  const loc = model.definitions['sap.common.Languages']?.name?.location || null;
837
- model.$messageFunctions.info('api-ignoring-language-assoc', loc, {
845
+ model.$messageFunctions.info( 'api-ignoring-language-assoc', loc, {
838
846
  '#': variant, option: 'addTextsLanguageAssoc', art: 'sap.common.Languages', name: 'code',
839
847
  }, {
840
848
  std: 'Ignoring option $(OPTION) because entity $(ART) is missing',
841
849
  code: 'Ignoring option $(OPTION) because entity $(ART) is missing element $(NAME)',
842
- });
850
+ } );
843
851
  }
844
852
 
845
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
 
@@ -38,6 +38,7 @@ const { cdsFs } = require('../utils/file');
38
38
 
39
39
  const fs = require('fs');
40
40
  const path = require('path');
41
+ const { CsnLocation, XsnSource } = require('./classes');
41
42
 
42
43
  const extensionParsers = {
43
44
  csn: parseCsn.parse,
@@ -52,7 +53,7 @@ const extensionParsers = {
52
53
  // `errors`: vector of errors (file IO or ArgumentError)
53
54
  class InvocationError extends Error {
54
55
  constructor( errs, ...args ) {
55
- super(...args);
56
+ super( ...args );
56
57
  this.code = 'ERR_CDS_COMPILER_INVOCATION';
57
58
  this.errors = errs;
58
59
  this.hasBeenReported = false;
@@ -63,7 +64,7 @@ class InvocationError extends Error {
63
64
  // `argument`: the command argument (repeated file names)
64
65
  class ArgumentError extends Error {
65
66
  constructor( arg, ...args ) {
66
- super(...args);
67
+ super( ...args );
67
68
  this.code = 'ERR_CDS_COMPILER_ARGUMENT';
68
69
  this.argument = arg;
69
70
  }
@@ -85,14 +86,15 @@ function parseX( source, filename, options = {}, messageFunctions = null ) {
85
86
  const ext = path.extname( filename ).slice(1).toLowerCase();
86
87
  // eslint-disable-next-line no-nested-ternary
87
88
  const parser = options.fallbackParser === 'auto!'
88
- ? (source?.startsWith('{') ? parseCsn.parse : parseLanguage)
89
+ ? (source?.startsWith( '{' ) ? parseCsn.parse : parseLanguage)
89
90
  : (extensionParsers[ext] || extensionParsers[options.fallbackParser] ||
90
- source.startsWith('{') && parseCsn.parse);
91
+ source.startsWith( '{' ) && parseCsn.parse);
91
92
  if (parser)
92
93
  return parser( source, filename, options, messageFunctions );
93
94
 
94
- const model = { location: { file: filename } };
95
- messageFunctions.error( 'file-unknown-ext', emptyWeakLocation(filename),
95
+ const model = new XsnSource();
96
+ model.location = new CsnLocation( filename );
97
+ messageFunctions.error( 'file-unknown-ext', emptyWeakLocation( filename ),
96
98
  { file: ext, '#': !ext && 'none' }, {
97
99
  std: 'Unknown file extension $(FILE)',
98
100
  none: 'No file extension',
@@ -129,29 +131,30 @@ function parseX( source, filename, options = {}, messageFunctions = null ) {
129
131
  // - 'string' or instanceof Buffer: the file content
130
132
  // - { realname: fs.realpath(filename) }: if filename is not canonicalized
131
133
  //
132
- function compileX( filenames, dir = '', options = {}, fileCache = Object.create(null) ) {
134
+ function compileX( filenames, dir = '', options = {}, fileCache = Object.create( null ) ) {
133
135
  // A non-proper dictionary (i.e. with prototype) is safe if the keys are
134
136
  // absolute file names - they start with `/` or `\` or similar
135
137
  // if (Object.getPrototypeOf( fileCache ))
136
138
  // fileCache = Object.assign( Object.create(null), fileCache );
137
- dir = path.resolve(dir);
139
+ dir = path.resolve( dir );
138
140
  const model = { sources: null, options };
139
141
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
142
+ const { resolveModule } = makeModuleResolver( options, fileCache, model.$messageFunctions );
140
143
  let input = null;
141
144
 
142
145
  let all = processFilenames( filenames, dir )
143
- .then((processedInput) => {
146
+ .then( (processedInput) => {
144
147
  input = processedInput;
145
148
  model.sources = input.sources;
146
- })
147
- .then(() => promiseAllDoNotRejectImmediately( input.files.map(readAndParse) ))
149
+ } )
150
+ .then( () => promiseAllDoNotRejectImmediately( input.files.map( readAndParse ) ) )
148
151
  .then( testInvocation, (reason) => {
149
152
  // do not reject with PromiseAllError, use InvocationError:
150
153
  const errs = reason.valuesOrErrors.filter( e => e instanceof Error );
151
154
  // internal error if no file IO error (has property `path`)
152
155
  return Promise.reject( errs.find( e => !e.path ) ||
153
156
  new InvocationError( [ ...input.repeated, ...errs ]) );
154
- });
157
+ } );
155
158
 
156
159
  if (!options.parseOnly && !options.parseCdl)
157
160
  all = all.then( readDependencies );
@@ -159,7 +162,7 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
159
162
  return all.then( () => {
160
163
  moduleLayers.setLayers( input.sources );
161
164
  return compileDoX( model );
162
- });
165
+ } );
163
166
 
164
167
  // Read file `filename` and parse its content, return messages
165
168
  async function readAndParse( filename ) {
@@ -171,12 +174,12 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
171
174
  return []; // no further dependency processing
172
175
  // no parallel readAndParse with same resolved filename should read the file,
173
176
  // also ensure deterministic sequence in sources:
174
- sources[filename] = { location: { file: rel } };
177
+ sources[filename] = { location: new CsnLocation( rel ) };
175
178
 
176
179
  const source = await cdsFs( fileCache, options.traceFs ).readFileAsync( filename, 'utf8' );
177
180
  const ast = parseX( source, rel, options, model.$messageFunctions );
178
181
  sources[filename] = ast;
179
- ast.location = { file: rel };
182
+ ast.location = new CsnLocation( rel );
180
183
  ast.dirname = path.dirname( filename );
181
184
  assertConsistency( ast, options );
182
185
 
@@ -208,10 +211,8 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
208
211
  }
209
212
  // create promises after all usingFroms have been collected, as the
210
213
  // Promise executor is called immediately with `new`:
211
- for (const module in dependencies) {
212
- promises.push( resolveModule( dependencies[module], fileCache, options,
213
- model.$messageFunctions ) );
214
- }
214
+ for (const module in dependencies)
215
+ promises.push( resolveModule( dependencies[module] ) );
215
216
  }
216
217
  if (!promises.length)
217
218
  return [];
@@ -234,23 +235,25 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
234
235
  * @param {object} [fileCache]
235
236
  * @returns {XSN.Model} Augmented CSN
236
237
  */
237
- function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.create(null) ) {
238
+ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.create( null ) ) {
238
239
  // A non-proper dictionary (i.e. with prototype) is safe if the keys are
239
240
  // absolute file names - they start with `/` or `\` or similar
240
- dir = path.resolve(dir);
241
+ dir = path.resolve( dir );
241
242
  const a = processFilenamesSync( filenames, dir );
242
243
 
243
244
  const model = { sources: a.sources, options };
244
245
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
246
+ const { resolveModuleSync } = makeModuleResolverSync( options, fileCache,
247
+ model.$messageFunctions );
245
248
 
246
249
  const asts = [];
247
250
  const errors = [];
248
251
  a.files.forEach( val => readAndParseSync( val, (err, ast) => {
249
252
  if (err)
250
- errors.push(err);
253
+ errors.push( err );
251
254
  if (ast)
252
- asts.push(ast);
253
- }));
255
+ asts.push( ast );
256
+ } ) );
254
257
 
255
258
  if (errors.length || a.repeated.length) {
256
259
  // internal error if no file IO error (has property `path`)
@@ -264,12 +267,12 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
264
267
  asts.length = 0;
265
268
  // Push dependencies to `ast`. Only works because readAndParseSync() is synchronous.
266
269
  for (const fileName of fileNames) {
267
- readAndParseSync(fileName, ( err, ast ) => {
270
+ readAndParseSync( fileName, ( err, ast ) => {
268
271
  if (err)
269
272
  throw err;
270
273
  if (ast)
271
274
  asts.push( ast );
272
- });
275
+ } );
273
276
  }
274
277
  }
275
278
  }
@@ -280,36 +283,36 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
280
283
  // Read file `filename` and parse its content, return messages
281
284
  function readAndParseSync( filename, cb ) {
282
285
  if ( filename === false ) { // module which has not been found
283
- cb(null, null);
286
+ cb( null, null );
284
287
  return;
285
288
  }
286
289
  const rel = a.sources[filename] || path.relative( dir, filename );
287
290
  if (typeof rel === 'object') { // already parsed
288
- cb(null, null);
291
+ cb( null, null );
289
292
  return; // no further dependency processing
290
293
  }
291
294
  // no parallel readAndParse with same resolved filename should read the file,
292
295
  // also ensure deterministic sequence in a.sources:
293
- a.sources[filename] = { location: { file: rel } };
296
+ a.sources[filename] = { location: new CsnLocation( rel ) };
294
297
 
295
298
  cdsFs( fileCache, options.traceFs ).readFileSync( filename, 'utf8', (err, source) => {
296
299
  if (err) {
297
- cb(err, null);
300
+ cb( err, null );
298
301
  }
299
302
  else {
300
303
  try {
301
304
  const ast = parseX( source, rel, options, model.$messageFunctions );
302
305
  a.sources[filename] = ast;
303
- ast.location = { file: rel };
306
+ ast.location = new CsnLocation( rel );
304
307
  ast.dirname = path.dirname( filename );
305
308
  assertConsistency( ast, options );
306
309
  cb( null, ast );
307
310
  }
308
311
  catch (e) {
309
- cb(e, null);
312
+ cb( e, null );
310
313
  }
311
314
  }
312
- });
315
+ } );
313
316
  }
314
317
 
315
318
  function readDependenciesSync( astArray ) {
@@ -329,10 +332,8 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
329
332
  }
330
333
  // create promises after all usingFroms have been collected, as the
331
334
  // Promise executor is called immediately with `new`:
332
- for (const module in dependencies) {
333
- fileNames.push( resolveModuleSync( dependencies[module], fileCache, options,
334
- model.$messageFunctions ) );
335
- }
335
+ for (const module in dependencies)
336
+ fileNames.push( resolveModuleSync( dependencies[module] ) );
336
337
  }
337
338
  if (!fileNames.length)
338
339
  return [];
@@ -364,7 +365,7 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
364
365
  function compileSourcesX( sourcesDict, options = {} ) {
365
366
  if (typeof sourcesDict === 'string')
366
367
  sourcesDict = { '<stdin>.cds': sourcesDict };
367
- const sources = Object.create(null);
368
+ const sources = Object.create( null );
368
369
  const model = { sources, options };
369
370
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
370
371
 
@@ -373,7 +374,7 @@ function compileSourcesX( sourcesDict, options = {} ) {
373
374
  if (typeof source === 'string') {
374
375
  const ast = parseX( source, filename, options, model.$messageFunctions );
375
376
  sources[filename] = ast;
376
- ast.location = { file: filename };
377
+ ast.location = new CsnLocation( filename );
377
378
  assertConsistency( ast, options );
378
379
  }
379
380
  else if (options.$xsnObjects) { // source is a XSN object with option $xsnObjects
@@ -382,7 +383,7 @@ function compileSourcesX( sourcesDict, options = {} ) {
382
383
  else { // source is a CSN object
383
384
  const ast = parseCsn.augment( source, filename, options, model.$messageFunctions );
384
385
  sources[filename] = ast;
385
- ast.location = { file: filename };
386
+ ast.location = new CsnLocation( filename );
386
387
  assertConsistency( ast, options );
387
388
  }
388
389
 
@@ -422,9 +423,9 @@ function recompileX( csn, options ) {
422
423
  // TODO: $recompile: true should be enough
423
424
 
424
425
  const file = csn.$location && csn.$location.file &&
425
- csn.$location.file.replace(/[.]cds$/, '.cds.csn') || '<recompile>.csn';
426
+ csn.$location.file.replace( /[.]cds$/, '.cds.csn' ) || '<recompile>.csn';
426
427
 
427
- const sources = Object.create(null);
428
+ const sources = Object.create( null );
428
429
  const model = { sources, options };
429
430
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
430
431
  // TODO: or use module which invokes the recompilation?
@@ -433,7 +434,7 @@ function recompileX( csn, options ) {
433
434
  moduleLayers.setLayers( sources );
434
435
  const compiled = compileDoX( model ); // calls throwWithError()
435
436
  if (options.messages) // does not help with exception in compileDoX()
436
- deduplicateMessages(options.messages); // TODO: do better
437
+ deduplicateMessages( options.messages ); // TODO: do better
437
438
  return compiled;
438
439
  }
439
440
 
@@ -478,12 +479,11 @@ function compileDoX( model ) {
478
479
  resolve( model );
479
480
  tweakAssocs( model );
480
481
  assertConsistency( model );
482
+ check( model );
481
483
  throwWithError();
482
484
  if (options.lintMode)
483
485
  return model;
484
486
 
485
- check(model);
486
- throwWithError();
487
487
  return propagator.propagate( model );
488
488
  }
489
489
 
@@ -498,7 +498,7 @@ function compileDoX( model ) {
498
498
  * not normalized - any strings work
499
499
  */
500
500
  async function processFilenames( filenames, dir ) {
501
- const filenameMap = Object.create(null);
501
+ const filenameMap = Object.create( null );
502
502
 
503
503
  const promises = [];
504
504
  for (const originalName of filenames) {
@@ -508,12 +508,12 @@ async function processFilenames( filenames, dir ) {
508
508
  // Resolve possible symbolic link; if the file does not exist
509
509
  // we just continue using the original name because readFile()
510
510
  // already handles non-existent files.
511
- const promise = fs.promises.realpath(path.resolve(dir, originalName))
512
- .then(setName, () => setName(originalName));
513
- promises.push(promise);
511
+ const promise = fs.promises.realpath( path.resolve( dir, originalName ) )
512
+ .then( setName, () => setName( originalName ) );
513
+ promises.push( promise );
514
514
  }
515
515
 
516
- await Promise.all(promises);
516
+ await Promise.all( promises );
517
517
  return createSourcesDict( filenames, filenameMap, dir );
518
518
  }
519
519
 
@@ -521,15 +521,15 @@ async function processFilenames( filenames, dir ) {
521
521
  * Synchronous version of processFilenames().
522
522
  */
523
523
  function processFilenamesSync( filenames, dir ) {
524
- const filenameMap = Object.create(null);
524
+ const filenameMap = Object.create( null );
525
525
 
526
526
  for (const originalName of filenames) {
527
- let name = path.resolve(dir, originalName);
527
+ let name = path.resolve( dir, originalName );
528
528
  try {
529
529
  // Resolve possible symbolic link; if the file does not exist
530
530
  // we just continue using the original name because readFile()
531
531
  // already handles non-existent files.
532
- name = fs.realpathSync.native(name);
532
+ name = fs.realpathSync.native( name );
533
533
  }
534
534
  catch (e) {
535
535
  // Ignore the not-found (ENOENT) error
@@ -551,7 +551,7 @@ function processFilenamesSync( filenames, dir ) {
551
551
  * @return {{sources: object, files: string[], repeated: ArgumentError[]}}
552
552
  */
553
553
  function createSourcesDict( filenames, filenameMap, dir ) {
554
- const sources = Object.create(null);
554
+ const sources = Object.create( null );
555
555
  const files = [];
556
556
  const repeated = [];
557
557
 
@@ -559,7 +559,7 @@ function createSourcesDict( filenames, filenameMap, dir ) {
559
559
  const name = filenameMap[originalName];
560
560
  if (!sources[name]) {
561
561
  sources[name] = path.relative( dir, name );
562
- files.push(name);
562
+ files.push( name );
563
563
  }
564
564
  else if (typeof sources[name] === 'string') { // not specified more than twice
565
565
  const msg = `Repeated argument: file '${ sources[name] }'`;