@sap/cds-compiler 6.5.2 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -13,6 +13,32 @@ we might not list every change in its behavior here.
13
13
  Productive code should never require a `beta` flag to be set, and
14
14
  might use a deprecated flag only for a limited period of time.
15
15
 
16
+ ## Version 6.6.0 - 2025-12-12
17
+
18
+ ### Added
19
+
20
+ - compiler:
21
+ + Support for upcoming ESlint rules (by other team) for Fiori elements annotations.
22
+ + Namespace `cds.dataproducts` is no longer reserved by the cds-compiler. It is used by the CAP @sap/cds-data-products plugin.
23
+ - for.odata/to.edm(x):
24
+ + Enumeration symbols are now supported in annotation expression syntax.
25
+ + For projections and views, the `@hierarchy` annotation now triggers generation of
26
+ additional Fiori Tree View relevant annotations and fields.
27
+ - for.effective: First non-beta release.
28
+
29
+ ### Changed
30
+
31
+ - `to.sql`: Annotating a foreign key of an association in a view with a sql-snippet annotation (e.g. `@sql.append`)
32
+ now results in an error. This is the default behaviour for any element in a view.
33
+
34
+ ### Fixed
35
+
36
+ - compiler:
37
+ + Minor fixes for auto-redirections and recompilation with localized data
38
+ in very rare situations when an aspect definition uses an entity as include.
39
+ + Don't let “namespaces” prevent the compiler to generate texts/target entities.
40
+ - to.sql: Improve foreign key flattening for various edge cases.
41
+
16
42
  ## Version 6.5.2 - 2025-12-02
17
43
 
18
44
  ### Fixed
package/lib/api/main.js CHANGED
@@ -321,9 +321,6 @@ function forSeal( csn, options, messageFunctions ) {
321
321
  function forEffective( csn, options, messageFunctions ) {
322
322
  const internalOptions = prepareOptions.for.effective(options);
323
323
  internalOptions.transformation = 'effective';
324
- // for.effective is still beta mode
325
- if (!baseModel.isBetaEnabled(options, 'effectiveCsn'))
326
- throw new baseError.CompilerAssertion('effective CSN is only supported with beta flag `effectiveCsn`!');
327
324
 
328
325
  return forEffectiveInternal(csn, options, internalOptions, messageFunctions);
329
326
  }
@@ -50,7 +50,8 @@ function isInReservedNamespace( absolute ) {
50
50
  !absolute.match( /^cds\.foundation(\.|$)/ ) &&
51
51
  !absolute.match( /^cds\.outbox(\.|$)/ ) && // Requested by Node runtime
52
52
  !absolute.match( /^cds\.core(\.|$)/ ) && // Requested by Node runtime
53
- !absolute.match( /^cds\.xt(\.|$)/ ); // Requested by Mtx
53
+ !absolute.match( /^cds\.xt(\.|$)/ ) && // Requested by Mtx
54
+ !absolute.match( /^cds\.dataproducts(\.|$)/ ); // Requested by @sap/cds-data-products
54
55
  }
55
56
 
56
57
  /**
@@ -735,7 +735,6 @@ const centralMessageTexts = {
735
735
  },
736
736
  'ref-undefined-art': {
737
737
  std: 'No artifact has been found with name $(ART)',
738
- namespace: 'No artifact has been found with name $(ART) which can be extended with annotations',
739
738
  localized: 'Can\'t extend localized definitions, only annotate them using an $(KEYWORD) statement',
740
739
  },
741
740
  // TODO: proposal 'No definition found for $(NAME)',
package/lib/base/model.js CHANGED
@@ -24,7 +24,6 @@ const availableBetaFlags = {
24
24
  mapAssocToJoinCardinality: true, // only SAP HANA HEX engine supports it
25
25
  enableUniversalCsn: true,
26
26
  odataTerms: true,
27
- effectiveCsn: true,
28
27
  tenantVariable: true,
29
28
  calcAssoc: true,
30
29
  temporalRawProjection: true,
@@ -38,6 +38,14 @@ function checkSqlAnnotationOnElement( member, memberName, prop, path ) {
38
38
  checkValidAnnoValue(member, '@sql.append', path, this.error, this.options);
39
39
  }
40
40
  }
41
+
42
+ // recursive check for keys
43
+ if (member.keys) {
44
+ for (const keyName of Object.keys(member.keys)) {
45
+ const key = member.keys[keyName];
46
+ checkSqlAnnotationOnElement.call(this, key, keyName, prop, path.concat([ 'keys', keyName ]));
47
+ }
48
+ }
41
49
  }
42
50
 
43
51
  /**
@@ -93,7 +93,6 @@ const forRelationalDBCsnValidators = [
93
93
  navigationIntoMany,
94
94
  checkPathsInStoredCalcElement,
95
95
  featureFlags,
96
- checkAndRemoveEnums,
97
96
  ];
98
97
  /**
99
98
  * @type {Array<(query: CSN.Query, path: CSN.Path) => void>}
@@ -200,8 +199,11 @@ function _validate( csn, that,
200
199
  function getDBCsnValidators( options ) {
201
200
  const validations = [ ...forRelationalDBCsnValidators ];
202
201
 
203
- if (options.transformation !== 'effective')
202
+ if (options.transformation !== 'effective') {
203
+ validations.push(checkAndRemoveEnums);
204
204
  validations.push(checkForParams.csnValidator);
205
+ }
206
+
205
207
  if (options.sqlDialect === 'h2' || options.sqlDialect === 'postgres')
206
208
  validations.push(checkForHanaTypes);
207
209
 
@@ -528,7 +528,7 @@ function define( model ) {
528
528
  if (!reInit) // not for auto-exposed entity
529
529
  initArtifactParentLink( art, model.definitions );
530
530
  checkRedefinition( art );
531
- initDollarSelf( art ); // $self
531
+ initDollarSelf( art ); // $self, TODO: also for 'namespace'?
532
532
  initMembers( art );
533
533
  if (art.params)
534
534
  initDollarParameters( art ); // $parameters
@@ -78,6 +78,7 @@ function extend( model ) {
78
78
  extendArtifactBefore,
79
79
  extendArtifactAfter,
80
80
  extendForeignKeys,
81
+ withLocalizedData,
81
82
  applyIncludes, // TODO: re-check
82
83
  } );
83
84
 
@@ -940,32 +941,28 @@ function extend( model ) {
940
941
  }
941
942
 
942
943
  function checkRemainingMainExtensions( art, ext ) {
943
- const refCtx = ext.kind === 'annotate' && hasSecurityAnno( ext )
944
- ? 'annotate-sec'
945
- : ext.kind;
944
+ const refCtx = extensionRefContext( ext );
946
945
  if (!resolvePath( ext.name, refCtx, ext )) // error for extend, info for annotate
947
946
  return;
948
947
 
949
- if (art?.builtin) {
948
+ if (art?.builtin) { // TODO: do via accept
950
949
  info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location?
951
950
  }
952
- else if (ext.kind === 'extend' && art?.kind === 'namespace') {
953
- // `annotate` on namespaces already handled before
954
- const hasAnnotations = Object.keys(ext).find(a => a.charAt(0) === '@');
955
- const firstAnno = ext[hasAnnotations];
956
- // In v5, extending namespaces is only allowed for `extend with definitions`.
957
- // Neither annotations nor other extensions are allowed.
958
- // Non-artifact extensions are reported in resolvePath() already (for v5).
959
- // Because "namespaces" are the same as "unknown" artifacts in CSN, we don't report
960
- // an error for `annotate`s.
961
- // FIXME: The compiler generates empty `annotate` statements for
962
- // `extend ns with definitions {…}`. That's why we check the frontend.
963
- if (hasAnnotations || (!ext.artifacts && ext._block.$frontend !== 'json')) {
964
- error( 'ref-undefined-art', [ (firstAnno?.name || ext.name).location, ext ], {
965
- '#': 'namespace', art: ext,
966
- } );
967
- }
951
+ }
952
+
953
+ function extensionRefContext( ext ) {
954
+ if (ext.kind === 'annotate') {
955
+ if (hasSecurityAnno( ext ))
956
+ return 'annotate-sec';
968
957
  }
958
+ else if (ext.artifacts || // extend … with definitions
959
+ ext._block.$frontend === 'json' && !ext.elements && !ext.actions) {
960
+ // TODO: not for `extend context` and `extend service` → !ext.expectedKind
961
+ // TODO v7: also fully with CSN input
962
+ if (!ext.doc && !Object.keys( ext ).some( a => a.charAt(0) === '@') )
963
+ return 'annotate'; // TODO: or an extra refCtx ?
964
+ }
965
+ return ext.kind;
969
966
  }
970
967
 
971
968
  // Issue messages for annotations on namespaces and builtins
@@ -1530,10 +1527,11 @@ function extend( model ) {
1530
1527
 
1531
1528
  if (!art._ancestors && !art.query)
1532
1529
  setLink( art, '_ancestors', [] ); // recursive array of includes
1530
+ const shouldSetAncestors = art.kind === 'entity' && !art.query;
1533
1531
  for (const ref of ext.includes) {
1534
1532
  const template = resolvePath( ref, 'include', art );
1535
1533
  // !template -> non-includable, e.g. scalar type, or cyclic
1536
- if (template && !art.query) {
1534
+ if (template?.kind === 'entity' && shouldSetAncestors) {
1537
1535
  if (template._ancestors)
1538
1536
  art._ancestors.push( ...template._ancestors );
1539
1537
  art._ancestors.push( template );
@@ -1581,8 +1579,17 @@ function extend( model ) {
1581
1579
  if (template[prop] && !ext[prop])
1582
1580
  ext[prop] = Object.create( null );
1583
1581
  const location = weakRefLocation( ref );
1582
+
1583
+ // prevent recompilation issue when localized entity is included in aspect or
1584
+ // entity without keys (via shadowing):
1585
+ const checkForLocalized = prop === 'elements' &&
1586
+ template.kind === 'entity' && !template.query &&
1587
+ withLocalizedData( template, ext );
1588
+
1584
1589
  // eslint-disable-next-line no-loop-func
1585
1590
  forEachInOrder( template, prop, ( origin, name ) => {
1591
+ if (checkForLocalized && (name === 'texts' || name === 'localized'))
1592
+ return;
1586
1593
  if (members && members[name]) {
1587
1594
  if (!includesNonShadowedFirst && !ext[prop][name])
1588
1595
  dictAdd( ext[prop], name, members[name] ); // to keep order
@@ -1601,7 +1608,7 @@ function extend( model ) {
1601
1608
  if (origin.value && origin.$syntax === 'calc') {
1602
1609
  // TODO: If paths become invalid in the new artifact, should we mark
1603
1610
  // all usages in the expressions? Possibly just the first one?
1604
- // TODO: Unify with coding in extend.js
1611
+ // TODO: Unify with other code in extend.js
1605
1612
  elem.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
1606
1613
  elem.$syntax = 'calc';
1607
1614
  createAndLinkCalcDepElement( elem );
@@ -1628,6 +1635,31 @@ function extend( model ) {
1628
1635
  }
1629
1636
  }
1630
1637
 
1638
+ /**
1639
+ * Return true if include (with `name` and `elements`) in `ext` highly likely
1640
+ * contain elements `texts` and `localized` which are compiler-generated for
1641
+ * localized data. Due to recompilation, we cannot just check the `$inferred`
1642
+ * property in the XSN, but need to apply an heuristics.
1643
+ *
1644
+ * It is as follows: the elements `texts` and `localized` in entity `‹E›` and
1645
+ * the entity `‹E›.texts` are considered compiler-generated for Localized Data if
1646
+ *
1647
+ * - `‹E›.texts` has a key element `locale`
1648
+ * - `texts` of `‹E›` is an unmanaged composition to `‹E›.texts`
1649
+ * - `localized` of `‹E›` is an unmanaged association to `‹E›.texts`
1650
+ */
1651
+ function withLocalizedData( { name, elements }, ext ) {
1652
+ const textsEntityName = `${ name.id }.texts`;
1653
+ if (!model.definitions[textsEntityName]?.elements?.locale?.key?.val)
1654
+ return false;
1655
+ const { texts, localized } = elements ?? {};
1656
+ return texts?.target && localized?.target && texts.on && localized.on &&
1657
+ resolveUncheckedPath( texts.target, 'target', ext ) === textsEntityName &&
1658
+ resolveUncheckedPath( localized.target, 'target', ext ) === textsEntityName &&
1659
+ resolveUncheckedPath( texts.type, 'type', ext ) === 'cds.Composition' &&
1660
+ resolveUncheckedPath( localized.type, 'type', ext ) === 'cds.Association';
1661
+ }
1662
+
1631
1663
  /**
1632
1664
  * Report duplicates in parent[prop] that happen due to multiple includes having the
1633
1665
  * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
@@ -173,8 +173,8 @@ function generate( model ) {
173
173
  const localized = localizedData( art, textsEntity, fioriEnabled );
174
174
  if (!localized)
175
175
  return;
176
- if (textsEntity) // expanded localized data in source
177
- return; // -> make it idempotent
176
+ if (textsEntity && textsEntity.kind !== 'namespace')
177
+ return; // expanded texts entity in source -> nothing to do
178
178
  createTextsEntity( art, textsName, localized, fioriEnabled );
179
179
  addTextsAssociations( art, textsName, localized );
180
180
  }
@@ -236,7 +236,10 @@ function generate( model ) {
236
236
  return false;
237
237
  }
238
238
 
239
- if (textsEntity) {
239
+ if (textsEntity?.kind === 'namespace') { // namespace Base.texts
240
+ textsEntity = null;
241
+ }
242
+ else if (textsEntity) {
240
243
  if (textsEntity.$duplicates)
241
244
  return false;
242
245
  if (textsEntity.kind !== 'entity' || textsEntity.query ||
@@ -283,15 +286,19 @@ function generate( model ) {
283
286
  */
284
287
  function createTextsEntity( base, absolute, textElems, fioriEnabled ) {
285
288
  const location = weakLocation( base.elements[$location] || base.location );
286
- const art = {
289
+ let art = {
287
290
  kind: 'entity',
288
291
  name: { id: absolute, location },
289
292
  location,
290
293
  elements: Object.create( null ),
291
294
  $inferred: 'localized-entity',
292
295
  };
296
+ const gap = model.definitions[absolute];
297
+ if (gap)
298
+ art = Object.assign( gap, art );
299
+ else
300
+ model.definitions[absolute] = art;
293
301
  setLink( art, '_block', model.$internal );
294
- model.definitions[absolute] = art;
295
302
  extendArtifactBefore( art ); // having extensions here would be wrong
296
303
 
297
304
  if (!fioriEnabled) {
@@ -341,6 +348,11 @@ function generate( model ) {
341
348
  addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue );
342
349
 
343
350
  initMainArtifact( art );
351
+ // do the kick-start relevant stuff: _service, there are no _ancestors,
352
+ // _descendants would have been set already for a gap artifact
353
+ setLink( art, '_service', art._parent._service );
354
+ model.$compositionTargets[absolute] = true;
355
+
344
356
  if (art.includes) {
345
357
  // add elements `locale`, etc. which are required below.
346
358
  applyIncludes( art, art ); // TODO: rethink - can we avoid this if only new extend?
@@ -641,7 +653,8 @@ function generate( model ) {
641
653
  'An aspect $(TARGET) with an element named $(NAME) can\'t be used as target' );
642
654
  return false;
643
655
  }
644
- if (model.definitions[entityName]) { // TODO: allow a gap (namespace)?
656
+ const place = model.definitions[entityName];
657
+ if (place && place.kind !== 'namespace') {
645
658
  error( null, [ location, elem ], { art: entityName },
646
659
  // eslint-disable-next-line @stylistic/max-len
647
660
  'Target entity $(ART) can\'t be created as there is another definition with this name' );
@@ -687,7 +700,7 @@ function generate( model ) {
687
700
  $inferred: 'aspect-composition',
688
701
  };
689
702
 
690
- const art = {
703
+ let art = {
691
704
  kind: 'entity',
692
705
  name: {
693
706
  id: entityName,
@@ -698,6 +711,12 @@ function generate( model ) {
698
711
  elements: Object.create( null ),
699
712
  $inferred: 'composition-entity',
700
713
  };
714
+ const gap = model.definitions[entityName];
715
+ if (gap)
716
+ art = Object.assign( gap, art );
717
+ else
718
+ model.definitions[entityName] = art;
719
+
701
720
  if (target.name) { // named target aspect
702
721
  if (!isDeprecatedEnabled( options, 'noCompositionIncludes' )) {
703
722
  art.includes = [ createInclude( target.name.id, location ) ];
@@ -741,9 +760,13 @@ function generate( model ) {
741
760
  addProxyElements( art, target.elements, 'aspect-composition', enforceLocation && location );
742
761
 
743
762
  setLink( art, '_block', model.$internal );
744
- model.definitions[entityName] = art;
745
763
  initMainArtifact( art );
746
764
 
765
+ // do the kick-start relevant stuff: _service, there are no _ancestors,
766
+ // _descendants would have been set already for a gap artifact
767
+ setLink( art, '_service', art._parent._service );
768
+ model.$compositionTargets[entityName] = true;
769
+
747
770
  // Apply annotations to generated artifact, prepare (not apply!) element
748
771
  // annotations (remark: adding elements is not allowed for generated artifacts):
749
772
  extendArtifactBefore( art );
@@ -495,8 +495,8 @@ function compileDoXSync( model ) {
495
495
  return model;
496
496
  }
497
497
  extend( model );
498
- generate( model );
499
498
  kickStart( model );
499
+ generate( model );
500
500
  populate( model );
501
501
 
502
502
  model.definitions = model.$functions.shuffleDict( model.definitions );
@@ -546,7 +546,7 @@ async function compileDoX( model ) {
546
546
  return model;
547
547
  }
548
548
 
549
- for (const phase of [ extend, generate, kickStart, populate ]) {
549
+ for (const phase of [ extend, kickStart, generate, populate ]) {
550
550
  phase( model );
551
551
  // eslint-disable-next-line no-await-in-loop
552
552
  await checkAsyncAbortFlag( options.abortSignal );
@@ -2,6 +2,7 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ const { builtinLocation } = require('../base/location');
5
6
  const { isBetaEnabled, forEachGeneric } = require('../base/model');
6
7
  const {
7
8
  setLink,
@@ -14,14 +15,12 @@ function kickStart( model ) {
14
15
  const { options } = model;
15
16
  const { message } = model.$messageFunctions;
16
17
 
17
- const { resolveUncheckedPath, resolvePath } = model.$functions;
18
+ const { resolveUncheckedPath, initMainArtifact } = model.$functions;
18
19
 
19
20
  // Set _service link (sorted to set it on parent first). Could be set
20
21
  // directly, but beware a namespace becoming a service later.
21
22
  Object.keys( model.definitions ).sort().forEach( setAncestorsAndService );
22
23
  forEachGeneric( model, 'definitions', postProcessArtifact );
23
-
24
- forEachGeneric( model, 'sources', resolveUsings );
25
24
  return;
26
25
 
27
26
 
@@ -85,7 +84,7 @@ function kickStart( model ) {
85
84
  chain.push( art );
86
85
  setLink( art, '_ancestors', 0 ); // avoid infloop with cyclic from
87
86
  const name = resolveUncheckedPath( art.query.from, 'from', art );
88
- art = name && model.definitions[name];
87
+ art = name && (model.definitions[name] || createGapArtifact( name ));
89
88
  if (autoexposed)
90
89
  break; // only direct projection for auto-exposed
91
90
  }
@@ -98,6 +97,18 @@ function kickStart( model ) {
98
97
  }
99
98
  }
100
99
 
100
+ function createGapArtifact( name, location = builtinLocation() ) {
101
+ // TODO: make it probably part of define.js
102
+ // TODO: make it work without location (or value undefined/null)
103
+ // TODO: change the location later if overwritten
104
+ const art = {
105
+ kind: 'namespace', name: { id: name, location }, location,
106
+ };
107
+ model.definitions[name] = art;
108
+ initMainArtifact( art );
109
+ return art;
110
+ }
111
+
101
112
  function postProcessArtifact( art ) {
102
113
  tagCompositionTargets( art );
103
114
  if (art.$queries) {
@@ -128,40 +139,19 @@ function kickStart( model ) {
128
139
  }
129
140
 
130
141
  function tagCompositionTargets( elem ) {
142
+ // TODO: together with test for targetIsTargetAspect()
131
143
  if (elem.target && isDirectComposition( elem )) {
132
144
  // A target aspect would have already moved to property `targetAspect` in
133
145
  // define.js (hm... more something for kick-start.js...)
134
146
  // TODO: for safety, just use resolveUncheckedPath()
135
- const target = resolvePath( elem.target, 'target', elem );
147
+ const target = resolveUncheckedPath( elem.target, 'target', elem );
136
148
  if (target)
137
- model.$compositionTargets[target.name.id] = true;
149
+ model.$compositionTargets[target] = true;
138
150
  }
139
151
  if (elem.targetAspect?.elements)
140
152
  elem = elem.targetAspect;
141
153
  forEachGeneric( elem, 'elements', tagCompositionTargets );
142
154
  }
143
-
144
- // Resolve the using declarations in `using`. Issue
145
- // error message if the referenced artifact does not exist.
146
- // TODO: think of moving this to resolve.js
147
- function resolveUsings( src, topLevel ) {
148
- if (!src.usings)
149
- return;
150
- for (const def of src.usings) {
151
- if (def.usings) // using {...}
152
- resolveUsings( def );
153
- if (!def.name || !def.name.id)
154
- continue; // using {...}, parse error
155
- const art = model.definitions[def.name.absolute];
156
- if (art && art.$duplicates)
157
- continue;
158
- const ref = def.extern;
159
- const user = (topLevel ? def : src);
160
- const from = user.fileDep;
161
- if (art || !from || from.realname) // no error for non-existing ref with non-existing module
162
- resolvePath( ref, 'using', def ); // TODO: consider FROM for validNames
163
- }
164
- }
165
155
  }
166
156
 
167
157
  module.exports = kickStart;
@@ -47,7 +47,7 @@ function propagate( model ) {
47
47
  precision: always,
48
48
  scale: always,
49
49
  srid: always,
50
- localized: withKind,
50
+ localized,
51
51
  target: notWithExpand,
52
52
  targetAspect,
53
53
  cardinality: notWithExpand,
@@ -78,6 +78,7 @@ function propagate( model ) {
78
78
  const { rewriteAnnotationsRefs, rewriteRefsInExpression } = xprRewriteFns( model );
79
79
 
80
80
  const { message, throwWithError } = model.$messageFunctions;
81
+ const { withLocalizedData } = model.$functions;
81
82
 
82
83
  forEachDefinition( model, run );
83
84
  forEachGeneric( model, 'vocabularies', run );
@@ -336,6 +337,24 @@ function propagate( model ) {
336
337
  annotation( prop, target, source );
337
338
  }
338
339
 
340
+ /**
341
+ * Propagate `localized`, but not for a texts entity
342
+ * (as `null` in an Universal CSN).
343
+ */
344
+ function localized( prop, target, source ) {
345
+ const main = target._main;
346
+ if (target.kind === 'element' && main.kind === 'entity' && !main.query) {
347
+ const { id } = main.name;
348
+ const base = id.endsWith( '.texts' ) &&
349
+ model.definitions[id.slice( 0, -'.texts'.length )];
350
+ if (base && withLocalizedData( base, main )) {
351
+ target.localized = { $inferred: 'NULL', val: undefined }; // null in UCSN
352
+ return;
353
+ }
354
+ }
355
+ withKind( prop, target, source );
356
+ }
357
+
339
358
  function withKind( prop, target, source ) {
340
359
  if (target.kind === 'param' && source.kind === 'entity')
341
360
  return; // Don't propagate from entity types to parameters (+ return type).
@@ -108,8 +108,32 @@ function resolve( model ) {
108
108
  const ignoreSpecifiedElements
109
109
  = isDeprecatedEnabled( options, 'ignoreSpecifiedQueryElements' );
110
110
 
111
+ forEachGeneric( model, 'sources', resolveUsings );
111
112
  return doResolve();
112
113
 
114
+ /**
115
+ * Resolve the using declarations in `using`.
116
+ * Issue error message if the referenced artifact does not exist.
117
+ */
118
+ function resolveUsings( src, topLevel ) {
119
+ if (!src.usings)
120
+ return;
121
+ for (const def of src.usings) {
122
+ if (def.usings) // using {...}
123
+ resolveUsings( def );
124
+ if (!def.name || !def.name.id)
125
+ continue; // using {...}, parse error
126
+ const art = model.definitions[def.name.absolute];
127
+ if (art && art.$duplicates)
128
+ continue;
129
+ const ref = def.extern;
130
+ const user = (topLevel ? def : src);
131
+ const from = user.fileDep;
132
+ if (art || !from || from.realname) // no error for non-existing ref with non-existing module
133
+ resolvePath( ref, 'using', def ); // TODO: consider FROM for validNames
134
+ }
135
+ }
136
+
113
137
  function doResolve() {
114
138
  // Phase 1: check paths in `usings` has been moved to kick-start.js Phase 2:
115
139
  // calculate/init view elements & collect views in order:
@@ -846,6 +846,8 @@ function fns( model ) {
846
846
 
847
847
  if (!semantics.dollar) {
848
848
  valid.push( dynamicDict );
849
+ if (isMainRef) // eslint-disable-next-line no-return-assign
850
+ valid.forEach( ( d, idx ) => (valid[idx] = removeGapArtifact( d )) );
849
851
  }
850
852
  else {
851
853
  const filterFn = semantics.variableFilter || removeRestrictedVariables;
@@ -908,14 +910,22 @@ function fns( model ) {
908
910
  }
909
911
  // need to do that here, because we also need to disallow Service.AutoExposed:elem
910
912
  // TODO: but Service.AutoExposed.NotAuto should be fine
911
- if (isMainRef !== 'all' && artItemsCount === 0 &&
912
- art.$inferred === 'autoexposed' && !user.$inferred) {
913
- // Depending on the processing sequence, the following could be a
914
- // simple 'ref-undefined-art'/'ref-undefined-def' - TODO: which we
915
- // could "change" to this message at the end of compile():
916
- error( 'ref-unexpected-autoexposed', [ item.location, user ], { art },
917
- 'An auto-exposed entity can\'t be referred to - expose entity $(ART) explicitly' );
918
- return null; // continuation semantics: like “not found”
913
+ if (isMainRef && isMainRef !== 'all' && artItemsCount === 0) {
914
+ if (art.kind === 'namespace') {
915
+ if (env !== false) {
916
+ semantics.notFound( user, item, [ removeGapArtifact( env ) ],
917
+ null, prev, path, semantics );
918
+ }
919
+ return null;
920
+ }
921
+ else if (art.$inferred === 'autoexposed' && !user.$inferred) {
922
+ // Depending on the processing sequence, the following could be a
923
+ // simple 'ref-undefined-art'/'ref-undefined-def' - TODO: which we
924
+ // could "change" to this message at the end of compile():
925
+ error( 'ref-unexpected-autoexposed', [ item.location, user ], { art },
926
+ 'An auto-exposed entity can\'t be referred to - expose entity $(ART) explicitly' );
927
+ return null; // continuation semantics: like “not found”
928
+ }
919
929
  }
920
930
  }
921
931
  return art;
@@ -978,7 +988,25 @@ function fns( model ) {
978
988
  return def;
979
989
  if (def.$duplicates)
980
990
  return false;
981
- return setArtifactLink( head, def ); // we do not want to see the using
991
+ art = setArtifactLink( head, def ); // we do not want to see the using
992
+ if (art.kind !== 'namespace')
993
+ return art;
994
+ }
995
+ /* FALLTHROUGH */
996
+ case 'namespace': {
997
+ if (semantics.isMainRef === 'all' || path.length !== 1 && ref.scope !== 1)
998
+ return art;
999
+ const valid = [];
1000
+ const lexical = userBlock( user );
1001
+ if (lexical) {
1002
+ for (let env = lexical; env; env = env._block)
1003
+ valid.push( removeGapArtifact( env.artifacts || Object.create( null ) ) );
1004
+ }
1005
+ valid.push( removeGapArtifact( model.definitions ) );
1006
+ semantics.notFound?.( user._user || user, head, valid, model.definitions,
1007
+ null, path, semantics );
1008
+
1009
+ return null;
982
1010
  }
983
1011
  case 'mixin': {
984
1012
  // use a source element having that name if in `extend … with columns`:
@@ -2153,6 +2181,18 @@ function removeDollarNames( dict ) {
2153
2181
  return r;
2154
2182
  }
2155
2183
 
2184
+ function removeGapArtifact( dict ) {
2185
+ const r = Object.create( null );
2186
+ for (const name in dict) {
2187
+ const art = dict[name];
2188
+ // TODO: for gaps with sub artifacts, we use use `${name}.` as name
2189
+ // TODO: clarify with LSP
2190
+ if (art.kind !== 'namespace' || art._subArtifacts)
2191
+ r[name] = dict[name];
2192
+ }
2193
+ return r;
2194
+ }
2195
+
2156
2196
  module.exports = {
2157
2197
  fns,
2158
2198
  };
@@ -206,6 +206,7 @@ function initExprAnnoBlock( art, block ) {
206
206
 
207
207
  function initDollarSelf( art ) {
208
208
  // TODO: use setMemberParent() ?
209
+ // TODO: also on 'namespace's? (test with annotation with checked ref on them)
209
210
  const self = {
210
211
  name: { id: '$self', location: art.location },
211
212
  kind: '$self',