@sap/cds-compiler 4.9.6 → 5.1.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/bin/cds_remove_invalid_whitespace.js +2 -1
  3. package/bin/cdsc.js +49 -19
  4. package/bin/cdshi.js +3 -1
  5. package/doc/CHANGELOG_BETA.md +7 -0
  6. package/lib/api/main.js +16 -19
  7. package/lib/api/options.js +5 -14
  8. package/lib/api/trace.js +0 -1
  9. package/lib/base/builtins.js +1 -0
  10. package/lib/base/location.js +4 -1
  11. package/lib/base/message-registry.js +43 -29
  12. package/lib/base/messages.js +23 -26
  13. package/lib/base/meta.js +10 -0
  14. package/lib/base/model.js +0 -2
  15. package/lib/base/node-helpers.js +0 -1
  16. package/lib/base/optionProcessorHelper.js +11 -0
  17. package/lib/checks/dbFeatureFlags.js +5 -0
  18. package/lib/checks/enricher.js +1 -5
  19. package/lib/checks/structuredAnnoExpressions.js +30 -0
  20. package/lib/checks/validator.js +8 -0
  21. package/lib/compiler/assert-consistency.js +5 -1
  22. package/lib/compiler/base.js +1 -1
  23. package/lib/compiler/builtins.js +18 -2
  24. package/lib/compiler/checks.js +2 -5
  25. package/lib/compiler/define.js +8 -8
  26. package/lib/compiler/extend.js +108 -37
  27. package/lib/compiler/generate.js +1 -1
  28. package/lib/compiler/index.js +27 -10
  29. package/lib/compiler/lsp-api.js +501 -2
  30. package/lib/compiler/populate.js +60 -13
  31. package/lib/compiler/propagator.js +10 -8
  32. package/lib/compiler/resolve.js +117 -94
  33. package/lib/compiler/shared.js +114 -32
  34. package/lib/compiler/tweak-assocs.js +31 -21
  35. package/lib/compiler/utils.js +2 -1
  36. package/lib/compiler/xsn-model.js +4 -0
  37. package/lib/edm/annotations/genericTranslation.js +69 -35
  38. package/lib/edm/csn2edm.js +16 -4
  39. package/lib/edm/edm.js +10 -3
  40. package/lib/edm/edmAnnoPreprocessor.js +1 -2
  41. package/lib/edm/edmPreprocessor.js +8 -10
  42. package/lib/gen/Dictionary.json +66 -2
  43. package/lib/gen/language.checksum +1 -1
  44. package/lib/gen/language.interp +2 -1
  45. package/lib/gen/languageParser.js +4995 -4817
  46. package/lib/json/csnVersion.js +1 -1
  47. package/lib/json/from-csn.js +4 -7
  48. package/lib/json/to-csn.js +25 -12
  49. package/lib/language/antlrParser.js +2 -2
  50. package/lib/language/errorStrategy.js +0 -1
  51. package/lib/language/genericAntlrParser.js +35 -12
  52. package/lib/language/multiLineStringParser.js +3 -2
  53. package/lib/language/textUtils.js +1 -0
  54. package/lib/main.d.ts +28 -9
  55. package/lib/main.js +9 -9
  56. package/lib/model/cloneCsn.js +1 -0
  57. package/lib/model/csnRefs.js +22 -5
  58. package/lib/model/csnUtils.js +0 -14
  59. package/lib/model/revealInternalProperties.js +1 -2
  60. package/lib/modelCompare/compare.js +13 -11
  61. package/lib/optionProcessor.js +30 -9
  62. package/lib/render/manageConstraints.js +1 -1
  63. package/lib/render/toCdl.js +44 -14
  64. package/lib/render/toHdbcds.js +1 -2
  65. package/lib/render/toSql.js +45 -8
  66. package/lib/render/utils/common.js +12 -9
  67. package/lib/render/utils/stringEscapes.js +1 -0
  68. package/lib/transform/db/applyTransformations.js +13 -8
  69. package/lib/transform/db/associations.js +62 -54
  70. package/lib/transform/db/backlinks.js +20 -5
  71. package/lib/transform/db/expansion.js +1 -6
  72. package/lib/transform/db/flattening.js +86 -109
  73. package/lib/transform/db/killAnnotations.js +3 -0
  74. package/lib/transform/db/processSqlServices.js +63 -0
  75. package/lib/transform/db/temporal.js +3 -4
  76. package/lib/transform/db/views.js +0 -1
  77. package/lib/transform/draft/odata.js +56 -3
  78. package/lib/transform/effective/annotations.js +3 -2
  79. package/lib/transform/effective/flattening.js +135 -0
  80. package/lib/transform/effective/main.js +6 -4
  81. package/lib/transform/effective/types.js +13 -9
  82. package/lib/transform/forOdata.js +0 -2
  83. package/lib/transform/forRelationalDB.js +9 -19
  84. package/lib/transform/localized.js +7 -8
  85. package/lib/transform/odata/flattening.js +39 -31
  86. package/lib/transform/odata/typesExposure.js +5 -17
  87. package/lib/transform/transformUtils.js +1 -1
  88. package/lib/transform/translateAssocsToJoins.js +0 -1
  89. package/lib/utils/file.js +87 -8
  90. package/lib/utils/moduleResolve.js +59 -8
  91. package/lib/utils/term.js +3 -2
  92. package/package.json +7 -3
  93. package/share/messages/message-explanations.json +2 -0
  94. package/share/messages/type-unexpected-foreign-keys.md +52 -0
  95. package/share/messages/type-unexpected-on-condition.md +52 -0
@@ -67,6 +67,7 @@ const {
67
67
  } = require('./utils');
68
68
 
69
69
  const detectCycles = require('./cycle-detector');
70
+ const { CompilerAssertion } = require('../base/error');
70
71
 
71
72
  const $location = Symbol.for( 'cds.$location' );
72
73
 
@@ -88,8 +89,8 @@ function resolve( model ) {
88
89
  } = model.$messageFunctions;
89
90
  const {
90
91
  resolvePath,
92
+ resolveDefinitionName,
91
93
  traverseExpr,
92
- createRemainingAnnotateStatements,
93
94
  effectiveType,
94
95
  getOrigin,
95
96
  getInheritedProp,
@@ -130,8 +131,6 @@ function resolve( model ) {
130
131
  resolveDefinitionName( model.sources[name].namespace );
131
132
  }
132
133
 
133
- // create “super” ANNOTATE statements for annotations on unknown artifacts:
134
- createRemainingAnnotateStatements();
135
134
  // report cyclic dependencies:
136
135
  detectCycles( model.definitions, ( user, art, location, semanticLoc ) => {
137
136
  if (location) {
@@ -157,68 +156,102 @@ function resolve( model ) {
157
156
  // Phase 2+3: calculate propagated KEYs
158
157
  //--------------------------------------------------------------------------
159
158
 
159
+ /**
160
+ * Set `_projection` links in navigation elements and creates $navElement hierarchy.
161
+ *
162
+ * @param {XSN.Artifact} view
163
+ */
160
164
  function setNavigationProjections( view ) {
161
165
  if (!view.$queries)
162
166
  return;
163
167
  for (const query of view.$queries) {
164
- forEachGeneric( query, 'elements', ( elem ) => {
165
- if (!elem._origin || elem.expand || !elem.value)
168
+ // traversing sub-elements not necessary, since we're in a view
169
+ // TODO: Handle expand.
170
+ forEachGeneric( query, 'elements', function navProjectionsForElement( elem ) {
171
+ if (!elem._origin || elem.expand || !elem.value?.path)
166
172
  return;
167
173
  // TODO: what about elements where _origin is set without value?
168
174
  // TODO: or should we push elems with `expand` sibling to extra list for
169
175
  // better messages? (Whatever that means exactly.)
170
- const nav = pathNavigation( elem.value );
171
- const { path } = elem.value;
172
-
173
- if (elem._pathHead?.kind === '$inline' && path.length === 1) {
174
- const item = path[0];
175
- const hpath = elem._pathHead.value?.path;
176
- const head = hpath?.length === 1 && hpath[0]._navigation;
177
- // Alias .{ elem } - but not with Alias.{ $magic }: consider for ON-rewrite
178
- if (head?.kind === '$tableAlias' && item.id.charAt(0) !== '$')
179
- pushLink( head.elements[item.id], '_projections', elem );
180
- }
181
- else if (nav.navigation) { // not set for $self.…
182
- // Path could start with table alias; get start index
183
- let index = path.indexOf(nav.item);
184
- if (index === -1)
185
- return; // should not happen
186
176
 
187
- let navItem = nav.navigation;
188
- if (!nav.item._navigation) // first non-table-alias
189
- setLink( nav.item, '_navigation', navItem );
177
+ if (elem._pathHead) {
178
+ if (elem._pathHead?.kind !== '$inline')
179
+ // we're traversing top-level elements of the query; other _pathHead kinds can't happen
180
+ throw new CompilerAssertion('found unexpected "expand", but expected "inline"');
190
181
 
191
- if (path[index].where || path[index].args)
192
- return;
193
- ++index;
194
- while (navItem && index < path.length) {
195
- const step = path[index];
196
- if (!step?.id || step.where || step.args)
197
- break;
198
- if (!navItem.elements?.[step.id]) {
199
- const elements = navItem._origin?.elements ||
200
- navItem._origin?.target?._artifact?.elements;
201
- if (!elements)
202
- break;
203
- // Only link available path steps (navigation tree).
204
- const origin = elements[step.id];
205
- const member = linkToOrigin( origin, step.id, navItem, 'elements',
206
- navItem.path?.location, true );
207
- member.$inferred = 'expanded';
208
- member.kind = '$navElement';
209
- }
210
- navItem = navItem.elements[step.id];
211
- setLink( step, '_navigation', navItem );
212
- ++index;
182
+ if (!isPathBreakout( elem.value )) {
183
+ const fullPath = columnParentPath( elem );
184
+ if (fullPath)
185
+ setNavigationProjectionsForElementRef({ path: fullPath }, elem);
213
186
  }
214
- // Last path step, if found, is a simple projection
215
- if (index === path.length && navItem)
216
- pushLink( navItem, '_projections', elem );
187
+ }
188
+ else {
189
+ setNavigationProjectionsForElementRef( elem.value, elem );
217
190
  }
218
191
  } );
219
192
  }
220
193
  }
221
194
 
195
+ function setNavigationProjectionsForElementRef( ref, elem ) {
196
+ const { path } = ref;
197
+ const nav = pathNavigation( ref );
198
+ if (nav.navigation) { // not set for $self.…
199
+ // Path could start with table alias; get start index
200
+ let index = path.indexOf(nav.item);
201
+ if (index === -1)
202
+ return; // should not happen
203
+
204
+ let navItem = nav.navigation;
205
+ if (!nav.item._navigation) // first non-table-alias
206
+ setLink( nav.item, '_navigation', navItem );
207
+
208
+ if (path[index].where || path[index].args)
209
+ return;
210
+ ++index;
211
+ while (navItem && index < path.length) {
212
+ const step = path[index];
213
+ if (!step?.id || step.where || step.args)
214
+ break;
215
+ if (!navItem.elements?.[step.id]) {
216
+ const elements = navItem._origin?.elements ||
217
+ navItem._origin?.target?._artifact?.elements;
218
+ if (!elements)
219
+ break;
220
+ // Only link available path steps (navigation tree).
221
+ const origin = elements[step.id];
222
+ const member = linkToOrigin( origin, step.id, navItem, 'elements',
223
+ navItem.path?.location, true );
224
+ member.$inferred = 'expanded';
225
+ member.kind = '$navElement';
226
+ }
227
+ navItem = navItem.elements[step.id];
228
+ setLink( step, '_navigation', navItem );
229
+ ++index;
230
+ }
231
+ // Last path step, if found, is a simple projection
232
+ if (index === path.length && navItem)
233
+ pushLink( navItem, '_projections', elem );
234
+ }
235
+ }
236
+
237
+ function columnParentPath( elem ) {
238
+ if (!elem._pathHead || !elem.value?.path || isPathBreakout( elem.value ))
239
+ return elem.value?.path;
240
+
241
+ const fullPath = [ ...elem.value.path ];
242
+ let pathHead = elem._pathHead;
243
+ while (pathHead) {
244
+ if (pathHead.kind !== '$inline' || !pathHead.value?.path ||
245
+ isPathBreakout( pathHead.value )) {
246
+ // path breakout for e.g. `$self.{ foo }`, `1 as a .{ foo }`
247
+ return null;
248
+ }
249
+ fullPath.unshift(...pathHead.value.path);
250
+ pathHead = pathHead._pathHead;
251
+ }
252
+ return fullPath;
253
+ }
254
+
222
255
  function propagateKeyProps( view ) {
223
256
  // Second argument true ensure that `key` is only propagated along simple
224
257
  // view, i.e. ref or subquery in FROM, not UNION or JOIN.
@@ -255,9 +288,9 @@ function resolve( model ) {
255
288
  function inheritedSourceKeyProp( { value, _pathHead } ) {
256
289
  if (!value || !value.path)
257
290
  return null;
258
- const nav = pathNavigation( value );
291
+ const nav = !_pathHead && pathNavigation( value );
259
292
  const item = value.path[value.path.length - 1];
260
- if (nav.navigation && nav.item === item)
293
+ if (nav?.navigation && nav.item === item)
261
294
  return item._artifact?.key;
262
295
  if (value.path.length !== 1 || _pathHead?.kind !== '$inline')
263
296
  return null;
@@ -281,13 +314,11 @@ function resolve( model ) {
281
314
  const toMany = withAssociation( from, targetMaxNotOne, true );
282
315
  if (toMany) {
283
316
  propagateKeys = false;
284
- info( 'query-from-many', [ toMany.location, query ], { art: toMany },
285
- {
286
- // eslint-disable-next-line max-len
287
- std: 'Key properties are not propagated because a to-many association $(ART) is selected',
288
- // eslint-disable-next-line max-len
289
- element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected',
290
- } );
317
+ info( 'query-from-many', [ toMany.location, query ], { art: toMany }, {
318
+ std: 'Key properties are not propagated because a to-many association $(ART) is selected',
319
+ // eslint-disable-next-line max-len
320
+ element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected',
321
+ } );
291
322
  }
292
323
  // Check that all keys from the source are projected:
293
324
  const notProjected = []; // we actually push to the array
@@ -297,7 +328,7 @@ function resolve( model ) {
297
328
  if (nav.$duplicates)
298
329
  continue;
299
330
  const { key } = nav._origin;
300
- if (key && key.val && !(nav._projections && nav._projections.length))
331
+ if (key?.val && !nav._projections?.length)
301
332
  notProjected.push( nav.name.id );
302
333
  }
303
334
  if (notProjected.length) {
@@ -311,9 +342,12 @@ function resolve( model ) {
311
342
  // Check that there is no to-many assoc used in select item:
312
343
  for (const name in query.elements) {
313
344
  const elem = query.elements[name];
314
- if (!elem.$inferred && elem.value &&
315
- testExpr( elem.value, selectTest, () => false, elem ))
316
- propagateKeys = false;
345
+
346
+ if (!elem.$inferred && elem.value?.path) {
347
+ const path = elem._pathHead ? columnParentPath( elem ) : elem.value.path;
348
+ if (testExpr({ path }, selectTest, () => false, elem))
349
+ propagateKeys = false;
350
+ }
317
351
  }
318
352
  return propagateKeys;
319
353
 
@@ -321,15 +355,13 @@ function resolve( model ) {
321
355
  const art = withAssociation( expr, targetMaxNotOne );
322
356
  if (art) {
323
357
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
324
- info( 'query-navigate-many', [ art.location, user || query ], { art },
325
- {
326
- // eslint-disable-next-line max-len
327
- std: 'Navigating along to-many association $(ART) - key properties are not propagated',
328
- // eslint-disable-next-line max-len
329
- element: 'Navigating along to-many association $(MEMBER) of $(ART) - key properties are not propagated',
330
- // eslint-disable-next-line max-len
331
- alias: 'Navigating along to-many mixin association $(MEMBER) - key properties are not propagated',
332
- } );
358
+ info( 'query-navigate-many', [ art.location, user || query ], { art }, {
359
+ std: 'Navigating along to-many association $(ART) - key properties are not propagated',
360
+ // eslint-disable-next-line max-len
361
+ element: 'Navigating along to-many association $(MEMBER) of $(ART) - key properties are not propagated',
362
+ // eslint-disable-next-line max-len
363
+ alias: 'Navigating along to-many mixin association $(MEMBER) - key properties are not propagated',
364
+ } );
333
365
  }
334
366
  return art;
335
367
  }
@@ -442,7 +474,7 @@ function resolve( model ) {
442
474
  obj.items = items;
443
475
  obj.$expand = 'origin';
444
476
  }
445
- if (obj.items) { // TODO: make this a while in v5 (also items proxy)
477
+ if (obj.items) { // TODO: make this a while in v6 (also items proxy)
446
478
  obj = obj.items || obj; // the object which has type properties
447
479
  effectiveType( obj );
448
480
  }
@@ -510,14 +542,12 @@ function resolve( model ) {
510
542
  else if (obj.targetAspect && obj.targetAspect.elements) { // silent dependencies
511
543
  forEachGeneric( obj.targetAspect, 'elements', elem => dependsOnSilent( art, elem ) );
512
544
  }
545
+
513
546
  if (obj.foreignKeys) { // silent dependencies
514
547
  // Avoid strange ref-cyclic if managed composition is key (check comes later)
515
- // TODO: the following is already done by addImplicitForeignKeys()!
516
- if (obj.$inferred !== 'aspect-composition') {
517
- forEachGeneric( obj, 'foreignKeys', (elem) => {
518
- dependsOnSilent( art, elem );
519
- } );
520
- }
548
+ // Done by addImplicitForeignKeys() for implicit keys.
549
+ if (!art.foreignKeys?.[$inferred] && obj.$inferred !== 'aspect-composition')
550
+ forEachGeneric( obj, 'foreignKeys', elem => dependsOnSilent( art, elem ) );
521
551
  addForeignKeyNavigations( art );
522
552
  }
523
553
 
@@ -1266,7 +1296,6 @@ function resolve( model ) {
1266
1296
  { art: target, '#': variant, keyword: op || '' }, {
1267
1297
  std: 'Redirection involves the complex view $(ART)',
1268
1298
  target: 'The redirected target $(ART) is a complex view',
1269
- // eslint-disable-next-line max-len
1270
1299
  targetOp: 'The redirected target $(ART) is a complex view with $(KEYWORD)',
1271
1300
  } );
1272
1301
  break;
@@ -1360,19 +1389,6 @@ function resolve( model ) {
1360
1389
  // General resolver functions
1361
1390
  //--------------------------------------------------------------------------
1362
1391
 
1363
- // Resolve the n-1 path steps before the definition name for LSP.
1364
- function resolveDefinitionName( art ) {
1365
- const path = art?.name?.path;
1366
- if (!art || art._main || !path || path.length <= 1)
1367
- return;
1368
-
1369
- let name = art.name.id;
1370
- for (let i = path.length - 1; i > 0; --i) {
1371
- name = name.substring(0, name.length - path[i].id.length - 1);
1372
- setArtifactLink( path[i - 1], model.definitions[name] || false );
1373
- }
1374
- }
1375
-
1376
1392
  // Resolve the type and its arguments if applicable.
1377
1393
  function resolveTypeExpr( art, user ) {
1378
1394
  const typeArt = resolvePath( art.type, 'type', user );
@@ -1565,8 +1581,6 @@ function resolve( model ) {
1565
1581
  function pathNavigation( ref ) {
1566
1582
  // currently, indirectly projectable elements are not included - we might
1567
1583
  // keep it this way! If we want them to be included - be aware: cycles
1568
- if (!ref._artifact)
1569
- return {};
1570
1584
  let item = ref.path && ref.path[0];
1571
1585
  const root = item && item._navigation;
1572
1586
  if (!root)
@@ -1583,4 +1597,13 @@ function pathNavigation( ref ) {
1583
1597
  return { navigation: root.elements?.[item.id], item, tableAlias: root };
1584
1598
  }
1585
1599
 
1600
+ function isPathBreakout( ref ) {
1601
+ if (!ref.path?.[0])
1602
+ return false;
1603
+ if (ref.scope === 'param')
1604
+ return true;
1605
+ const nav = (ref.path[0]._navigation || ref.path[0]._artifact);
1606
+ return nav && (nav.kind === '$self' || ref.path[0].id.charAt(0) === '$');
1607
+ }
1608
+
1586
1609
  module.exports = resolve;
@@ -19,7 +19,6 @@ const {
19
19
  isAssocToPrimaryKeys,
20
20
  artifactRefLocation,
21
21
  } = require('./utils');
22
- const { isBetaEnabled } = require('../base/model');
23
22
 
24
23
  const $inferred = Symbol.for( 'cds.$inferred' );
25
24
  const $location = Symbol.for( 'cds.$location' );
@@ -56,13 +55,14 @@ function fns( model ) {
56
55
  lexical: userBlock,
57
56
  dynamic: modelDefinitions,
58
57
  notFound: undefinedForAnnotate,
58
+ accept: extendableArtifact,
59
59
  },
60
60
  extend: {
61
61
  isMainRef: 'no-generated',
62
62
  lexical: userBlock,
63
63
  dynamic: modelDefinitions,
64
64
  notFound: undefinedDefinition,
65
- accept: acceptRealArtifact,
65
+ accept: extendableArtifact,
66
66
  },
67
67
  _extensions: {
68
68
  isMainRef: 'all',
@@ -163,7 +163,7 @@ function fns( model ) {
163
163
  param: paramSemantics,
164
164
  },
165
165
  'limit-offset': 'limit-rows',
166
- // general element references -----------------------------------------------
166
+ // general element / variable references --------------------------------------
167
167
  where: {
168
168
  lexical: tableAliasesAndSelf,
169
169
  dollar: true,
@@ -279,7 +279,7 @@ function fns( model ) {
279
279
  annotation: { // annotation assignments
280
280
  lexical: justDollarAliases,
281
281
  dollar: true,
282
- dynamic: parentElements,
282
+ dynamic: parentElementsOrKeys,
283
283
  navigation: assocOnNavigation,
284
284
  noDep: true,
285
285
  notFound: undefinedParentElement,
@@ -298,6 +298,7 @@ function fns( model ) {
298
298
  }),
299
299
  },
300
300
  // TODO: introduce some kind of inheritance
301
+ // used by xpr-rewrite.js to resolve rewritten path roots.
301
302
  annoRewrite: { // annotation assignments
302
303
  lexical: justDollarAliases,
303
304
  dollar: true,
@@ -323,6 +324,7 @@ function fns( model ) {
323
324
  resolveTypeArgumentsUnchecked, // TODO: move to some other file
324
325
  resolvePathRoot,
325
326
  resolvePath,
327
+ resolveDefinitionName,
326
328
  checkExpr,
327
329
  checkOnCondition,
328
330
  navigationEnv,
@@ -405,6 +407,7 @@ function fns( model ) {
405
407
 
406
408
  const s = referenceSemantics[expected];
407
409
  const semantics = (typeof s === 'string') ? referenceSemantics[s] : s;
410
+ semantics.name = expected;
408
411
 
409
412
  const r = getPathRoot( ref, semantics, origUser );
410
413
  const root = r && acceptPathRoot( r, ref, semantics, origUser );
@@ -512,6 +515,25 @@ function fns( model ) {
512
515
  artifact.$typeArgs = undefined;
513
516
  }
514
517
 
518
+ // Resolve the n-1 path steps before the definition name for LSP.
519
+ function resolveDefinitionName( art ) {
520
+ const path = art?.name?.path;
521
+ if (!art || art._main || !path || path.length <= 1)
522
+ return;
523
+
524
+ // Don't resolve paths in an annotation as a definition!
525
+ const definitions = art.kind === 'annotation' ? model.vocabularies : model.definitions;
526
+
527
+ let name = art.name.id;
528
+ if (art.kind === 'namespace') // namespace-statements are ref-only.
529
+ setArtifactLink( path[path.length - 1], definitions[name] || false );
530
+
531
+ for (let i = path.length - 1; i > 0; --i) {
532
+ name = name.substring(0, name.length - path[i].id.length - 1);
533
+ setArtifactLink( path[i - 1], definitions[name] || false );
534
+ }
535
+ }
536
+
515
537
  function getPathRoot( { path, scope, location }, semantics, user ) {
516
538
  // TODO: use string value of isMainRef?
517
539
  const head = path[0];
@@ -524,8 +546,11 @@ function fns( model ) {
524
546
  ruser = ruser._outer;
525
547
 
526
548
  // Handle expand/inline, `type of`, :param, global (internally for CDL):
527
- if (user._pathHead && !semantics.isMainRef) // in expand/inline
549
+ if (user._pathHead && !semantics.isMainRef) { // in expand/inline
550
+ const { name } = semantics;
528
551
  semantics = semantics.nestedColumn();
552
+ semantics.name = name;
553
+ }
529
554
  if (typeof scope === 'string') { // typeOf, param, global
530
555
  semantics = semantics?.[scope] && semantics[scope]( ruser, path, location, semantics );
531
556
  if (!semantics) {
@@ -563,10 +588,13 @@ function fns( model ) {
563
588
  if (r)
564
589
  return setArtifactLink( head, r );
565
590
 
566
- if (!semantics.dollar)
591
+ if (!semantics.dollar) {
567
592
  valid.push( dynamicDict );
568
- else
569
- valid.push( model.$magicVariables.elements, removeDollarNames( dynamicDict ) );
593
+ }
594
+ else {
595
+ valid.push( removeInvalidMagicVariables( model.$magicVariables.elements, semantics ),
596
+ removeDollarNames( dynamicDict ) );
597
+ }
570
598
  // TODO: streamline function arguments (probably: user, path, semantics )
571
599
  const undef = semantics.notFound?.( user._user || user, head, valid, dynamicDict,
572
600
  !isMainRef && user._user && user._artifact,
@@ -629,7 +657,6 @@ function fns( model ) {
629
657
  // simple 'ref-undefined-art'/'ref-undefined-def' - TODO: which we
630
658
  // could "change" to this message at the end of compile():
631
659
  error( 'ref-unexpected-autoexposed', [ item.location, user ], { art },
632
- // eslint-disable-next-line max-len
633
660
  'An auto-exposed entity can\'t be referred to - expose entity $(ART) explicitly' );
634
661
  return null; // continuation semantics: like “not found”
635
662
  }
@@ -663,9 +690,13 @@ function fns( model ) {
663
690
  // Do not accept a lonely table alias and `$projection`
664
691
  // TODO: test table alias and mixin named `$projection`
665
692
  if (path.length !== 1 || user.expand || user.inline) {
666
- // Rewrite $projection to $self
667
- if (semantics.rewriteProjectionToSelf && art.kind === '$self' && path[0].id === '$projection')
693
+ if (semantics.rewriteProjectionToSelf &&
694
+ art.kind === '$self' && path[0].id === '$projection') {
695
+ // Rewrite $projection to $self
668
696
  path[0].id = '$self';
697
+ warning( 'ref-expecting-$self', [ path[0].location, user ],
698
+ { code: '$projection', newcode: '$self' });
699
+ }
669
700
  return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
670
701
  }
671
702
 
@@ -725,7 +756,14 @@ function fns( model ) {
725
756
  message( 'ref-obsolete-parameters', [ head.location, user ],
726
757
  { code: `$parameters.${ id }`, newcode: `:${ id }` },
727
758
  'Obsolete $(CODE) - replace by $(NEWCODE)' );
728
- // TODO: replace it in to-csn correspondingly, probably v5 or later in v4 ?
759
+ return art;
760
+ }
761
+ case 'builtin': {
762
+ if (art.name.id === '$at') {
763
+ warning( 'ref-deprecated-variable', [ head.location, user ],
764
+ { code: '$at', newcode: '$valid' },
765
+ '$(CODE) is deprecated; use $(NEWCODE) instead' );
766
+ }
729
767
  return art;
730
768
  }
731
769
  default:
@@ -868,6 +906,13 @@ function fns( model ) {
868
906
  return environment( useParent ? user._parent : user );
869
907
  }
870
908
 
909
+ function parentElementsOrKeys( user ) {
910
+ // annotations on foreign keys only ever have access to their keys (except of course via $self)
911
+ if (user.kind === 'key')
912
+ return user._parent?.foreignKeys || Object.create( null );
913
+ return parentElements( user );
914
+ }
915
+
871
916
  function queryElements( user ) {
872
917
  return environment( user );
873
918
  }
@@ -1113,11 +1158,11 @@ function fns( model ) {
1113
1158
  // TODO: if it becomes non-configurable, we can omit this warning
1114
1159
  let id = pathName( path );
1115
1160
  let head = path[0]._artifact || { _parent: art };
1116
- // eslint-disable-next-line no-cond-assign
1117
1161
  while ((head = head?._parent) && head.kind === 'builtin')
1118
1162
  id = `${ head.name.id }.${ id }`;
1119
1163
  const msgId = (art.$uncheckedElements) ? 'ref-unknown-var' : 'ref-undefined-var';
1120
- signalNotFound( msgId, [ item.location, user ], valid, { id }, semantics );
1164
+ signalNotFound( msgId, [ item.location, user ],
1165
+ removeInvalidMagicVariables( valid, semantics ), { id }, semantics );
1121
1166
  }
1122
1167
  else if (art.kind === 'aspect' && !art.name) { // anonymous target aspect - TODO: still?
1123
1168
  signalNotFound( 'ref-undefined-element', [ item.location, user ], valid,
@@ -1199,9 +1244,16 @@ function fns( model ) {
1199
1244
  : acceptElemOrVar( art, user, ref );
1200
1245
  }
1201
1246
 
1202
- function acceptElemOrVar( art, user, ref ) {
1247
+ function acceptElemOrVar( art, user, ref, semantics ) {
1203
1248
  const { path } = ref;
1204
1249
  if (art.kind === 'builtin') {
1250
+ if (art.$onlyInExprCtx && !art.$onlyInExprCtx.includes(semantics.name)) {
1251
+ error( 'ref-unexpected-var', [ ref.location, user ], {
1252
+ '#': art.$onlyInExprCtx[0], name: pathName( path ),
1253
+ });
1254
+ return null;
1255
+ }
1256
+
1205
1257
  if (user.expand || user.inline) {
1206
1258
  const location = (user.expand || user.inline)[$location];
1207
1259
  const code = (user.expand) ? '{ ‹expand› }' : '.{ ‹inline› }';
@@ -1234,16 +1286,19 @@ function fns( model ) {
1234
1286
  return art;
1235
1287
  }
1236
1288
 
1237
- function acceptRealArtifact( art, user, ref ) {
1289
+ /**
1290
+ * Returns true, if the artifact is a _real_ artifact that can be used for `extend`/`annotate`.
1291
+ */
1292
+ function extendableArtifact( art, user, ref ) {
1238
1293
  if (art.kind !== 'namespace')
1239
1294
  return art;
1240
- // For compatibility (≤v4), we accept `extend Unknown` without elements/actions/includes
1241
- // In v5, only allow `extend with definitions`.
1242
- if (!isBetaEnabled( model.options, 'v5preview' ) &&
1243
- !(user.elements || user.actions || user.includes))
1244
- return art;
1245
1295
  const { location } = ref.path[ref.path.length - 1];
1246
- signalNotFound( 'ref-undefined-def', [ location, user ], null, { art } );
1296
+ if (user.kind === 'extend' && !(user.elements || user.actions || user.includes))
1297
+ return art; // allow `extend with definitions` and empty extends
1298
+
1299
+ // for `annotate`, handle "namespaces" just like unknown artifacts: only emit a warning
1300
+ signalNotFound( user.kind === 'annotate' ? 'ext-undefined-def' : 'ref-undefined-def',
1301
+ [ location, user ], null, { art } );
1247
1302
  return false;
1248
1303
  }
1249
1304
 
@@ -1459,7 +1514,7 @@ function fns( model ) {
1459
1514
  // console.log('NAV:',expr.path.map(r=>r.id),self)
1460
1515
  if (self || self == null && columnRefStartsWithSelf( user )) {
1461
1516
  checkOnlyForeignKeyNavigation( user, expr.path, 0, 'self-' );
1462
- checkNoUnmanaged( expr, user, 'self-unmanaged' );
1517
+ checkNoUnmanaged( expr, user, true, 'self-unmanaged' );
1463
1518
  }
1464
1519
  // TODO: set navigation dependencies later to avoid both ref-cyclic and
1465
1520
  // ref-invalid-navigation/ref-unexpected-assoc
@@ -1469,18 +1524,20 @@ function fns( model ) {
1469
1524
  const { path } = expr;
1470
1525
  if (!path)
1471
1526
  return;
1472
- if (path?.[0]?._navigation?.kind !== '$tableAlias')
1527
+ const self = path?.[0]?._navigation?.kind !== '$tableAlias';
1528
+ if (self)
1473
1529
  checkOnlyForeignKeyNavigation( user, expr.path );
1474
- checkNoUnmanaged( expr, user );
1530
+ checkNoUnmanaged( expr, user, self );
1475
1531
  }
1476
1532
 
1477
1533
  function checkRefInQuery( expr, exprCtx, user ) {
1478
1534
  const { path } = expr;
1479
1535
  if (!path)
1480
1536
  return;
1481
- if (pathStartsWithSelf( expr ))
1537
+ const self = pathStartsWithSelf( expr );
1538
+ if (self)
1482
1539
  checkOnlyForeignKeyNavigation( user, expr.path, 0, 'self-' );
1483
- checkNoUnmanaged( expr, user );
1540
+ checkNoUnmanaged( expr, user, self );
1484
1541
  }
1485
1542
 
1486
1543
  function checkExpandInlineRef( art, user, ref ) {
@@ -1523,10 +1580,10 @@ function fns( model ) {
1523
1580
  }
1524
1581
  const index = userTargetElementPathIndex( user, path );
1525
1582
  checkOnlyForeignKeyNavigation( user, path, index );
1526
- if (ref._artifact?.on) {
1583
+ const last = path[path.length - 1];
1584
+ if (!last.where && ref._artifact?.on) { // filter already complained about
1527
1585
  const target = index > 0 && index < path.length && ref._artifact?.target;
1528
1586
  const msg = (target?._artifact === user._main) ? 'self' : 'unmanaged';
1529
- const last = path[path.length - 1];
1530
1587
  error( 'ref-unexpected-assoc', [ last.location, user ],
1531
1588
  { '#': msg, code: '= $self' } );
1532
1589
  }
@@ -1614,6 +1671,16 @@ function fns( model ) {
1614
1671
  index = checkCoveredByForeignKey( assoc, path, index, user, msgPrefix );
1615
1672
  }
1616
1673
  assoc = path[index]?._artifact;
1674
+ if (assoc?.target) {
1675
+ // testing this above is not enough: we would not complain about $self
1676
+ // assoc filter at end of ref with expand/inline. We might also move the
1677
+ // unmanaged test above to here.
1678
+ if (path[index]?.where) {
1679
+ error( 'ref-unexpected-assoc', [ path[index].location, user ],
1680
+ { '#': `${ msgPrefix }with-filter`, alias: '$self' } );
1681
+ return;
1682
+ }
1683
+ }
1617
1684
  }
1618
1685
  }
1619
1686
 
@@ -1635,7 +1702,6 @@ function fns( model ) {
1635
1702
  const txt = index >= path.length
1636
1703
  ? 'complete'
1637
1704
  : (isAssocToPrimaryKeys( assoc ) ? 'keys' : 'std');
1638
- // eslint-disable-next-line max-len
1639
1705
  error( 'ref-invalid-navigation', [ last.location, user ], {
1640
1706
  '#': msgPrefix + txt, art: assoc, name: last.id, alias: '$self',
1641
1707
  }, {
@@ -1653,10 +1719,12 @@ function fns( model ) {
1653
1719
  return path.length;
1654
1720
  }
1655
1721
 
1656
- function checkNoUnmanaged( ref, user, messageVariant = 'unmanaged' ) {
1722
+ function checkNoUnmanaged( ref, user, self, messageVariant = 'unmanaged' ) {
1657
1723
  if (ref._artifact?.on && !ref.$expected) {
1658
1724
  const { path } = ref;
1659
1725
  const last = path[path.length - 1];
1726
+ if (self && last.where) // already complained about filter
1727
+ return;
1660
1728
  error( 'ref-unexpected-assoc', [ last.location, user ],
1661
1729
  { '#': messageVariant, alias: '$self' } );
1662
1730
  }
@@ -1670,7 +1738,8 @@ function fns( model ) {
1670
1738
  * @param {string} msgId
1671
1739
  * @param {any} location
1672
1740
  * @param {object[]} valid
1673
- * @param {object} [textParams]
1741
+ * @param {object} [textParams]
1742
+ * @param {object} [semantics]
1674
1743
  */
1675
1744
  function signalNotFound( msgId, location, valid, textParams, semantics ) {
1676
1745
  if (location.$notFound) // TODO: still necessary?
@@ -1745,6 +1814,19 @@ function fns( model ) {
1745
1814
  }
1746
1815
  }
1747
1816
 
1817
+ function removeInvalidMagicVariables( variables, semantics ) {
1818
+ if (Array.isArray(variables))
1819
+ return variables.map(variable => removeInvalidMagicVariables( variable, semantics ));
1820
+
1821
+ const valid = Object.create(null);
1822
+ for (const name in variables) {
1823
+ const variable = variables[name];
1824
+ if (!variable.$onlyInExprCtx || variable.$onlyInExprCtx.includes( semantics.name ))
1825
+ valid[name] = variable;
1826
+ }
1827
+ return valid;
1828
+ }
1829
+
1748
1830
  function removeDollarNames( dict ) {
1749
1831
  const r = Object.create( null );
1750
1832
  for (const name in dict) {