@sap/cds-compiler 6.9.3 → 7.0.1

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 (69) hide show
  1. package/CHANGELOG.md +76 -2
  2. package/bin/cdsc.js +4 -33
  3. package/doc/IncompatibleChanges_v7.md +639 -0
  4. package/lib/api/main.js +4 -56
  5. package/lib/api/options.js +5 -15
  6. package/lib/api/validate.js +1 -0
  7. package/lib/base/builtins.js +1 -2
  8. package/lib/base/csnRefs.js +2 -6
  9. package/lib/base/message-registry.js +82 -76
  10. package/lib/base/messages.js +8 -5
  11. package/lib/base/optionProcessor.js +2 -72
  12. package/lib/base/specialOptions.js +20 -17
  13. package/lib/checks/defaultValues.js +1 -39
  14. package/lib/checks/hasPersistedElements.js +19 -3
  15. package/lib/checks/parameters.js +0 -34
  16. package/lib/checks/selectItems.js +2 -38
  17. package/lib/checks/typeParameters.js +162 -0
  18. package/lib/checks/validator.js +5 -8
  19. package/lib/compiler/assert-consistency.js +19 -5
  20. package/lib/compiler/checks.js +47 -43
  21. package/lib/compiler/define.js +6 -6
  22. package/lib/compiler/extend.js +102 -111
  23. package/lib/compiler/generate.js +4 -8
  24. package/lib/compiler/populate.js +4 -7
  25. package/lib/compiler/propagator.js +9 -9
  26. package/lib/compiler/resolve.js +205 -7
  27. package/lib/compiler/shared.js +76 -82
  28. package/lib/compiler/tweak-assocs.js +102 -22
  29. package/lib/compiler/utils.js +57 -12
  30. package/lib/compiler/xpr-rewrite.js +2 -15
  31. package/lib/edm/annotations/edmJson.js +14 -10
  32. package/lib/edm/annotations/genericTranslation.js +3 -1
  33. package/lib/edm/annotations/preprocessAnnotations.js +9 -26
  34. package/lib/edm/csn2edm.js +27 -20
  35. package/lib/edm/edmUtils.js +25 -0
  36. package/lib/gen/CdlGrammar.checksum +1 -1
  37. package/lib/gen/CdlParser.js +2237 -2241
  38. package/lib/gen/Dictionary.json +17 -2
  39. package/lib/json/from-csn.js +67 -52
  40. package/lib/json/to-csn.js +28 -25
  41. package/lib/language/textUtils.js +0 -13
  42. package/lib/main.d.ts +22 -59
  43. package/lib/main.js +1 -1
  44. package/lib/model/csnUtils.js +9 -8
  45. package/lib/parsers/AstBuildingParser.js +45 -55
  46. package/lib/parsers/Lexer.js +2 -0
  47. package/lib/parsers/identifiers.js +0 -9
  48. package/lib/render/toCdl.js +41 -40
  49. package/lib/render/toSql.js +8 -1
  50. package/lib/render/utils/common.js +1 -1
  51. package/lib/render/utils/sql.js +2 -3
  52. package/lib/tool-lib/enrichCsn.js +1 -2
  53. package/lib/transform/db/applyTransformations.js +7 -5
  54. package/lib/transform/db/assertUnique.js +8 -51
  55. package/lib/transform/db/associations.js +1 -1
  56. package/lib/transform/db/cdsPersistence.js +1 -15
  57. package/lib/transform/db/expansion.js +9 -12
  58. package/lib/transform/db/flattening.js +1 -1
  59. package/lib/transform/db/groupByOrderBy.js +0 -16
  60. package/lib/transform/db/views.js +57 -161
  61. package/lib/transform/draft/db.js +2 -2
  62. package/lib/transform/forOdata.js +25 -14
  63. package/lib/transform/forRelationalDB.js +93 -301
  64. package/lib/transform/localized.js +33 -102
  65. package/lib/transform/odata/flattening.js +11 -2
  66. package/lib/transform/transformUtils.js +25 -3
  67. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -2
  68. package/package.json +2 -2
  69. package/lib/render/toHdbcds.js +0 -1810
@@ -59,6 +59,7 @@ const {
59
59
  compositionTextVariant,
60
60
  targetCantBeAspect,
61
61
  userParam,
62
+ userQuery,
62
63
  forEachDefinition,
63
64
  forEachMember,
64
65
  forEachGeneric,
@@ -96,11 +97,15 @@ function resolve( model ) {
96
97
  getInheritedProp,
97
98
  hasTruthyProp, // limited inheritance
98
99
  resolveTypeArgumentsUnchecked,
100
+ userTargetElementPathIndex,
101
+ getQueryOperatorName,
102
+ traverseDollarSelfPairs,
99
103
  } = model.$functions;
100
104
  Object.assign( model.$functions, {
101
105
  addForeignKeyNavigations,
102
106
  redirectionChain,
103
107
  resolveExprInAnnotations,
108
+ checkBacklinkPartner,
104
109
  } );
105
110
 
106
111
  const ignoreSpecifiedElements
@@ -154,6 +159,8 @@ function resolve( model ) {
154
159
  // Phase 4: resolve all artifacts:
155
160
  forEachDefinition( model, resolveRefs );
156
161
  forEachGeneric( model, 'vocabularies', resolveRefs );
162
+ forEachDefinition( model, checkOpenBacklinkTargets ); // after checkBacklinkPartner()
163
+
157
164
  if (options.lspMode) {
158
165
  for (const name in model.sources)
159
166
  resolveDefinitionName( model.sources[name].namespace );
@@ -287,7 +294,7 @@ function resolve( model ) {
287
294
  }
288
295
  // Second argument controls whether `key` is only propagated along simple
289
296
  // view, i.e. ref or subquery in FROM, not UNION or JOIN.
290
- traverseQueryPost( view.query, !options.v7KeyPropagation, ( query ) => {
297
+ traverseQueryPost( view.query, !!options.v6KeyPropagation, ( query ) => {
291
298
  if (!withExplicitKeys( query ) && inheritKeyProp( query ) &&
292
299
  withKeyPropagation( query )) // now the part with messages
293
300
  inheritKeyProp( query, true );
@@ -357,10 +364,10 @@ function resolve( model ) {
357
364
  one: 'Key $(NAMES) has not been projected - key properties are not propagated',
358
365
  } );
359
366
  }
360
- if (options.v7KeyPropagation)
367
+ if (!options.v6KeyPropagation)
361
368
  return propagateKeys;
362
369
 
363
- // Check that there is no to-many assoc in the FROM reference or in the select
370
+ // v6: check that there is no to-many assoc in the FROM reference or in the select
364
371
  // item reference (references in expressions are not checked,
365
372
  // withAssociation() will see no _artifact links anyway)
366
373
  const toMany = withAssociation( from, targetMaxNotOne, true );
@@ -548,6 +555,7 @@ function resolve( model ) {
548
555
  }
549
556
  }
550
557
  if (obj.target) {
558
+ // TODO: introduce some resolveAssociation()
551
559
  if (!obj.target.$inferred || obj.target.$inferred === 'aspect-composition')
552
560
  resolveTarget( art, obj );
553
561
  else
@@ -1079,6 +1087,9 @@ function resolve( model ) {
1079
1087
  }
1080
1088
  }
1081
1089
 
1090
+ /**
1091
+ * Resolve user-provided target.
1092
+ */
1082
1093
  function resolveTarget( art, obj ) {
1083
1094
  if (art !== obj && obj.on) {
1084
1095
  // Unmanaged assoc inside items. Unmanaged assoc in param handled in resolveRefs()
@@ -1110,8 +1121,8 @@ function resolve( model ) {
1110
1121
  // TODO: also warning if inside structure
1111
1122
  }
1112
1123
  else { // if (obj.target._artifact)
1113
- // TODO: extra with $inferred (to avoid messages)?
1114
1124
  resolveExpr( obj.on, art.kind === 'mixin' ? 'mixin-on' : 'on', art );
1125
+ checkBacklinkPartner( obj );
1115
1126
  }
1116
1127
  }
1117
1128
  else if (art.kind === 'mixin') {
@@ -1138,6 +1149,187 @@ function resolve( model ) {
1138
1149
  }
1139
1150
  }
1140
1151
 
1152
+ function checkBareDollarSelf( assoc ) {
1153
+ traverseDollarSelfPairs( assoc.on, assoc, ( item, right, itemIsPath ) => {
1154
+ registerBacklink( item, right, itemIsPath );
1155
+ checkAssocOnSelf( item );
1156
+ checkAssocOnSelf( right );
1157
+ } );
1158
+ return;
1159
+
1160
+ function registerBacklink( item, right, itemIsPath ) {
1161
+ const backlinkPath = itemIsPath ? item : right;
1162
+ const last = backlinkPath.path[backlinkPath.path.length - 1];
1163
+ if (assoc.$backlink) {
1164
+ const location = combinedLocation( item.path[0], right.path[right.path.length - 1] );
1165
+ message( 'ref-duplicate-self-comparison', [ location, assoc ], { id: '$self' },
1166
+ 'Duplicate comparison with a bare $(ID) reference in an ON-condition' );
1167
+ return;
1168
+ }
1169
+ const backlinkRef = last?._artifact;
1170
+ if (backlinkRef?.target) {
1171
+ const backlink = { location: last.location, target: {} };
1172
+ setLink( backlink, '_origin', backlinkRef );
1173
+ setLink( backlink, '_outer', assoc );
1174
+ assoc.$backlink = backlink;
1175
+ }
1176
+ }
1177
+
1178
+ function checkAssocOnSelf( ref ) {
1179
+ // TODO: fully specify what `‹current_assoc›.‹backlink› = $self` means if the
1180
+ // target of ‹backlink› is not the current entity.
1181
+ // - what does it mean in an aspect
1182
+ // - how about auto-redirections/rewrite
1183
+ // TODO: also disallow if not in entity (
1184
+ const { path } = ref;
1185
+ if (path.length === 1 && path[0]._navigation?.kind === '$self') {
1186
+ if (assoc.on.$inferred)
1187
+ return;
1188
+ const query = userQuery( assoc );
1189
+ const main = query?._main;
1190
+ if (query && query !== main._leadingQuery) {
1191
+ const { txt, op } = getQueryOperatorName( query );
1192
+ const { location, id } = path[0];
1193
+ error( 'ref-unexpected-self', [ location, assoc ], { '#': txt, id, op } );
1194
+ }
1195
+ return;
1196
+ }
1197
+ const index = userTargetElementPathIndex( assoc, path );
1198
+ const target = index > 0 && index < path.length && ref._artifact?.target;
1199
+ if (!target && ref._artifact) {
1200
+ const last = path[path.length - 1];
1201
+ error( 'ref-expecting-target-assoc', [ last.location, assoc ], { id: '$self' },
1202
+ 'Only an association of the target side can be compared to $(ID)' );
1203
+ // TODO: ref-expecting-backlink 'Only a backlink association can ...'
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ /**
1209
+ * Check that the target T of the backlink association is related to the entity
1210
+ * (or other structure) E where `assoc` is embedded in: E is a valid redirection
1211
+ * target (without E having to be an entity) for T (or the "simple projection
1212
+ * ancestor" of T if `assoc` is managed, see below).
1213
+ *
1214
+ * Note: we do not forbid backlink comparisons in non-entities, because:
1215
+ *
1216
+ * - they might appear during recompilation case when including an entity,
1217
+ * - they are ok in an aspect used as target of a composition.
1218
+ *
1219
+ * Aspects can have _one_ "open backlink target", i.e. a target T which does not
1220
+ * fulfil the above condition. The check is then performed for all definitions
1221
+ * which include the aspect, see function checkOpenBacklinkTargets().
1222
+ *
1223
+ * A "simple projection ancestor" is the (recursive) query source of a simple
1224
+ * projection (one source, no assoc navigation, no query in FROM).
1225
+ *
1226
+ * If the condition is fulfilled, we can properly expand a backlink comparison,
1227
+ * taking potential renamings of source elements into account.
1228
+ */
1229
+ function checkBacklinkPartner( assoc ) {
1230
+ checkBareDollarSelf( assoc );
1231
+ // console.log('CBP:',assoc.kind,assoc._main?.name?.id,assoc.name.id,!!assoc.$backlink)
1232
+ const target = assoc.$backlink?._origin?.target?._artifact;
1233
+ if (!target)
1234
+ return;
1235
+ const main = assoc._main;
1236
+ const chain = main.name && redirectionChain( null, main, target, true );
1237
+ // Remark: an anonymous target in `targetAspect` does not have a name
1238
+ setLink( assoc.$backlink, '_redirected', chain );
1239
+ if (chain) // standard correct case
1240
+ return;
1241
+
1242
+ if (target._ancestors) { // && assocHasForeignKeys( assoc.$backlink?._origin )) {
1243
+ // with non-cyclic projection ancestors, not only for managed assoc, but also
1244
+ // unmanaged, see test3/Associations/Backlinks/CrossEyedBacklink.sql-err.cds
1245
+ // Current entity is no valid redirection target for the backlink association
1246
+ // → try whether it would be if its target is replaced with its “simple ancestor”
1247
+ // (simple projection, only managed assoc - TODO for tweak-assoc.js).
1248
+ // TODO: include this "go to ancestor" logic into redirectionChain()?
1249
+ // ...probably just if implicit key calculation is part of populate.js
1250
+ let ancestor = target;
1251
+ // TODO: warning!!!
1252
+ while (ancestor?.query?.from?._artifact?.kind === 'entity')
1253
+ ancestor = ancestor.query.from._artifact;
1254
+ // TODO: we should do the "simple projection ancestor" probably only if no
1255
+ // include is involved in the redirection chain
1256
+ if (redirectionChain( null, main, ancestor, true )) {
1257
+ warning( 'ref-dubious-backlink', [ assoc.$backlink.location, assoc ],
1258
+ { target },
1259
+ // eslint-disable-next-line @stylistic/max-len
1260
+ 'The target $(TARGET) of the backlink association has just a distant relationship to the current entity' );
1261
+ return;
1262
+ }
1263
+ }
1264
+ if (main.kind !== 'aspect') {
1265
+ message( 'ref-invalid-backlink', [ assoc.$backlink.location, assoc ], { target } );
1266
+ return;
1267
+ }
1268
+ if (!main.$backlink) {
1269
+ main.$backlink = { target: {} };
1270
+ setLink( main.$backlink.target, '_artifact', target );
1271
+ setLink( main.$backlink, '_associations', [] );
1272
+ }
1273
+ else if (target !== main.$backlink.target?._artifact) {
1274
+ message( 'ref-invalid-backlink', [ assoc.$backlink.location, assoc ],
1275
+ { '#': 'aspect', target } );
1276
+ if (main.$backlink.target) { // had been ok so far → complain about all
1277
+ for (const a of main.$backlink._associations) {
1278
+ message( 'ref-invalid-backlink', [ a.$backlink.location, a ],
1279
+ { '#': 'aspect', target: a.$backlink._origin.target._artifact } );
1280
+ }
1281
+ main.$backlink.target = undefined;
1282
+ }
1283
+ }
1284
+ main.$backlink._associations.push( assoc );
1285
+ }
1286
+
1287
+ /**
1288
+ * Check potential "open backlink target" of an aspect which is included into
1289
+ * `art`, see function checkBacklinkPartner(). Entities generated for managed
1290
+ * composition of anonymous aspects also check open backlink targets.
1291
+ */
1292
+ function checkOpenBacklinkTargets( art ) {
1293
+ const includes = art.includes ||
1294
+ art.$inferred === 'composition-entity' && [ anonymousAspectAsInclude( art._origin ) ];
1295
+ // Remark: how to detect wrong open backlink target inside an anonymous target aspect:
1296
+ // - direct: the pseudo include of the anonymous aspect is checked via this function
1297
+ // - recompilation: is not checked directly, but condition is copied to generated entity
1298
+ if (!includes)
1299
+ return;
1300
+ for (const incl of includes) {
1301
+ const backlink = incl._artifact?.$backlink;
1302
+ if (backlink?.target && !redirectionChain( null, art, backlink.target._artifact, true )) {
1303
+ message( 'ref-invalid-backlink', [ incl.location, art ],
1304
+ { '#': 'include', target: backlink.target } );
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ function anonymousAspectAsInclude( aspect ) {
1310
+ return { _artifact: aspect, location: aspect.location };
1311
+ }
1312
+
1313
+ // /**
1314
+ // * Return true if the association `assoc` is managed. Note: the foreign keys
1315
+ // * have not been calculated and propagated/rewritten yet.
1316
+ // */
1317
+ // function assocHasForeignKeys( assoc ) {
1318
+ // if (!assoc._effectiveType)
1319
+ // return null;
1320
+ // do {
1321
+ // if (assoc.foreignKeys)
1322
+ // return true;
1323
+ // if (assoc.on)
1324
+ // return false;
1325
+ // const max = assoc.cardinality?.targetMax;
1326
+ // if (max && (typeof max.val !== 'number' || max.val > 1))
1327
+ // return false;
1328
+ // // TODO: check newAssoc = managed[filter]
1329
+ // assoc = getOrigin( assoc );
1330
+ // } while (assoc);
1331
+ // return true;
1332
+ // }
1141
1333
 
1142
1334
  function checkRedirectedUserTarget( art ) {
1143
1335
  const issue = { target: art.target._artifact };
@@ -1326,6 +1518,11 @@ function resolve( model ) {
1326
1518
  }
1327
1519
  }
1328
1520
 
1521
+ /**
1522
+ * Check whether it is correct to redirect assoc `elem` to `target` (will always
1523
+ * be the case for auto-redirected assocs). Finally, set the `elem._redirected`
1524
+ * to the "redirection chain", i.e. an array of all entities involved.
1525
+ */
1329
1526
  // TODO: add this somehow to tweak-assocs.js ?
1330
1527
  function resolveRedirected( elem, target ) {
1331
1528
  setLink( elem, '_redirected', null ); // null = do not touch path steps after assoc
@@ -1508,7 +1705,7 @@ function resolve( model ) {
1508
1705
  }
1509
1706
 
1510
1707
  function resolveAnnoExpr( expr, art, anno = expr, topItem = false ) {
1511
- if (expr.$tokenTexts) {
1708
+ if (expr.$tokenTexts || expr.sym) {
1512
1709
  if (anno === expr) {
1513
1710
  initAnnotationForExpression( anno, art );
1514
1711
  }
@@ -1526,7 +1723,7 @@ function resolve( model ) {
1526
1723
  const withExpr = anno === expr &&
1527
1724
  // the following is needed for checkAnnotationAcceptsExpressions(),
1528
1725
  // otherwise @cds.persistence.skip: [hdi, hdbcds, sql.postgres] fails
1529
- expr.val.some( item => item.$tokenTexts || item.literal === 'struct');
1726
+ expr.val.some( item => item.$tokenTexts || item.sym || item.literal === 'struct');
1530
1727
  if (withExpr)
1531
1728
  initAnnotationForExpression( anno, art );
1532
1729
  expr.val.forEach( val => resolveAnnoExpr( val, art, anno, withExpr ) );
@@ -1725,7 +1922,8 @@ function resolve( model ) {
1725
1922
  setLink( sym, '_artifact', symbols[sym.id] );
1726
1923
  }
1727
1924
  else {
1728
- // inferred enums can't be extended (yet): show underlying enum
1925
+ setLink( sym, '_artifact', null );
1926
+ // inferred enums can't be extended (yet): show underlying enum - TODO: now possible
1729
1927
  while (type.enum[$inferred])
1730
1928
  type = getOrigin( type );
1731
1929
  const err = message( 'ref-undefined-enum', [ sym.location, user ],
@@ -48,10 +48,7 @@ function fns( model ) {
48
48
  lexical: null,
49
49
  dynamic: modelDefinitions,
50
50
  notFound: undefinedDefinition,
51
- messageMap: {
52
- 'ref-undefined-art': 'ref-undefined-using',
53
- 'ref-undefined-def': 'ref-undefined-using',
54
- },
51
+ messageMap: { 'ref-undefined-def': 'ref-undefined-using' },
55
52
  },
56
53
  // scope:'global': for cds.Association and auto-redirected targets
57
54
  $global: {
@@ -73,10 +70,7 @@ function fns( model ) {
73
70
  lexical: userBlock,
74
71
  dynamic: modelDefinitions,
75
72
  notFound: undefinedDefinition,
76
- messageMap: {
77
- 'ref-undefined-art': 'ext-undefined-art-sec',
78
- 'ref-undefined-def': 'ext-undefined-def-sec',
79
- },
73
+ messageMap: { 'ref-undefined-def': 'ext-undefined-def-sec' },
80
74
  accept: extendableArtifact,
81
75
  },
82
76
  extend: {
@@ -353,6 +347,9 @@ function fns( model ) {
353
347
  navigationEnv,
354
348
  nestedElements,
355
349
  attachAndEmitValidNames,
350
+ userTargetElementPathIndex,
351
+ getQueryOperatorName,
352
+ traverseDollarSelfPairs,
356
353
  } );
357
354
  traverseExpr.STOP = Symbol( 'STOP' );
358
355
  traverseExpr.SKIP = Symbol( 'SKIP' );
@@ -942,7 +939,7 @@ function fns( model ) {
942
939
  else if (art.$inferred === 'autoexposed' && !user.$inferred &&
943
940
  isMainRef !== 'no-leaf-gap') {
944
941
  // Depending on the processing sequence, the following could be a
945
- // simple 'ref-undefined-art'/'ref-undefined-def' - TODO: which we
942
+ // simple 'ref-undefined-def' - TODO: which we
946
943
  // could "change" to this message at the end of compile():
947
944
  // HINT: this does not appear anymore
948
945
  error( 'ref-unexpected-autoexposed', [ item.location, user ], { art },
@@ -1028,7 +1025,8 @@ function fns( model ) {
1028
1025
  for (let env = lexical; env; env = env._block)
1029
1026
  valid.push( removeGapArtifact( env.artifacts || Object.create( null ) ) );
1030
1027
  }
1031
- valid.push( removeGapArtifact( model.definitions, !definedViaCdl( user ) ) );
1028
+ const dynamicDict = semantics.dynamic( user );
1029
+ valid.push( removeGapArtifact( dynamicDict, !definedViaCdl( user ) ) );
1032
1030
  semantics.notFound?.( user._user || user, head, valid, model.definitions,
1033
1031
  null, path, semantics );
1034
1032
 
@@ -1373,10 +1371,14 @@ function fns( model ) {
1373
1371
  // Functions called via semantics.notFound: -----------------------------------
1374
1372
 
1375
1373
  function undefinedDefinition( user, item, valid, _dict, prev, _path, semantics ) {
1376
- // in a CSN source or for `using`, only one env was tested (valid.length 1) :
1377
- const art = (!prev) ? item.id : searchName( prev, item.id, 'absolute' );
1378
- signalNotFound( (valid.length > 1 ? 'ref-undefined-art' : 'ref-undefined-def'),
1379
- [ item.location, user ], valid, { art }, semantics );
1374
+ // in a CSN source, for `using`, or if not the first path item, only one env
1375
+ // was tested (valid.length 1) don't mention `using` declaration
1376
+ const name = (prev) ? `${ prev.name.id }.${ item.id }` : item.id;
1377
+ const msg = (valid.length > 1 && proposeUsingDecl( name, valid.at( -2 ) ))
1378
+ ? 'using'
1379
+ : 'std';
1380
+ signalNotFound( 'ref-undefined-def', [ item.location, user ], valid,
1381
+ { '#': msg, art: name, keyword: 'using' }, semantics );
1380
1382
  // TODO: improve text, use text variant for: "or builtin" or "definitions" or none
1381
1383
  }
1382
1384
 
@@ -1405,21 +1407,17 @@ function fns( model ) {
1405
1407
  name === 'DRAFT' && path?.length === 2 && path[1].id === 'DraftAdministrativeData' ||
1406
1408
  name.startsWith( 'localized.' )) // TODO: only if suffix is defined
1407
1409
  return;
1408
- signalNotFound( (valid.length > 1 ? 'ext-undefined-art' : 'ext-undefined-def'),
1409
- // TODO: ext-undefined-xyz
1410
+ signalNotFound( 'ext-undefined-def', // TODO: ext-undefined-xyz
1410
1411
  [ item.location, user ], valid, { art: name } );
1411
1412
  }
1412
1413
 
1413
1414
  function undefinedForExtend( user, item, valid, _dict, prev ) {
1414
1415
  // in a CSN source, only one env was tested (valid.length 1):
1415
1416
  const name = (prev) ? `${ prev.name.id }.${ item.id }` : item.id;
1416
- if (name.startsWith( 'localized.' )) {
1417
- error( 'ref-undefined-art', [ user.name.location || user.location, user ],
1418
- { '#': 'localized', keyword: 'annotate' } );
1419
- }
1420
- else {
1421
- undefinedDefinition( user, item, valid, _dict, prev );
1422
- }
1417
+ // TODO: do not “accept” `localized` as first path item for `extend`
1418
+ const msg = (name.startsWith( 'localized.' )) ? 'localized' : 'std';
1419
+ signalNotFound( 'ref-undefined-def', [ item.location, user ],
1420
+ valid, { '#': msg, art: name } );
1423
1421
  }
1424
1422
 
1425
1423
  function signalElementHint( user, item, valid, prev, semantics, path ) {
@@ -1879,35 +1877,26 @@ function fns( model ) {
1879
1877
  }
1880
1878
 
1881
1879
  // TODO: Don't allow path args and filter!
1882
- function checkOnCondition( expr, exprCtx, user ) {
1883
- if (!expr || expr.$inferred)
1884
- return;
1885
- const { op } = expr;
1886
- let { args } = expr;
1887
- if (!op || !args) {
1888
- checkExpr( expr, exprCtx, user );
1889
- return;
1890
- }
1891
- if (op?.val === '=') // TMP
1892
- args = [ args[0], { val: '=', literal: 'token' }, args[1] ];
1880
+ function checkOnCondition( expr, exprCtx, user ) { // TODO: move to tweak-assoc.js (or resolve.js)
1881
+ traverseDollarSelfPairs( expr, user, onPair, onLeaf );
1893
1882
 
1894
- for (let index = 0; index < args.length; ++index) {
1895
- const item = args[index];
1896
- const eq = args[index + 1];
1897
- if (eq?.val === '=' && eq.literal === 'token' && item.path && !item.scope) {
1898
- const right = args[index + 2];
1899
- if (right?.path && !right.scope &&
1900
- (isDollarSelfPair( item, right, user ) || isDollarSelfPair( right, item, user ))) {
1901
- checkAssocOnSelf( item, exprCtx, user );
1902
- checkAssocOnSelf( right, exprCtx, user );
1903
- index += 2;
1904
- continue;
1905
- }
1906
- }
1907
- checkOnCondition( item, exprCtx, user );
1883
+ function onPair( item, right ) {
1884
+ checkDollarSelfPairNavigation( item, user );
1885
+ checkDollarSelfPairNavigation( right, user );
1886
+ }
1887
+ function onLeaf( leaf ) {
1888
+ checkExpr( leaf, exprCtx, user );
1908
1889
  }
1909
1890
  }
1910
1891
 
1892
+ function checkDollarSelfPairNavigation( ref, user ) {
1893
+ const { path } = ref;
1894
+ if (path.length === 1 && path[0]._navigation?.kind === '$self')
1895
+ return;
1896
+ const index = userTargetElementPathIndex( user, path );
1897
+ checkOnlyForeignKeyNavigation( user, path, index );
1898
+ }
1899
+
1911
1900
  // // standard element reference check;
1912
1901
  // function checkElementStd( art, _user, ref, semantics ) {
1913
1902
  // // No further checks on navigation: nothing to do
@@ -1982,6 +1971,36 @@ function fns( model ) {
1982
1971
  return userTargetElementPathIndex( user, path ) > 0;
1983
1972
  }
1984
1973
 
1974
+ function traverseDollarSelfPairs( expr, user, onPair, onLeaf ) {
1975
+ const { op } = expr;
1976
+ let { args } = expr;
1977
+ if (!op || !args) {
1978
+ if (onLeaf)
1979
+ onLeaf( expr );
1980
+ return;
1981
+ }
1982
+ if (op.val === '=') // TMP
1983
+ args = [ args[0], { val: '=', literal: 'token' }, args[1] ];
1984
+
1985
+ for (let index = 0; index < args.length; ++index) {
1986
+ const item = args[index];
1987
+ const eq = args[index + 1];
1988
+ if (eq?.val === '=' && eq.literal === 'token' && item.path && !item.scope) {
1989
+ const right = args[index + 2];
1990
+ if (right?.path && !right.scope) {
1991
+ const itemIsPath = isDollarSelfPair( item, right, user );
1992
+ const rightIsPath = !itemIsPath && isDollarSelfPair( right, item, user );
1993
+ if (itemIsPath || rightIsPath) {
1994
+ onPair( item, right, itemIsPath );
1995
+ index += 2;
1996
+ continue;
1997
+ }
1998
+ }
1999
+ }
2000
+ traverseDollarSelfPairs( item, user, onPair, onLeaf );
2001
+ }
2002
+ }
2003
+
1985
2004
  function checkAssocOn( ref, exprCtx, user ) {
1986
2005
  const { path } = ref;
1987
2006
  if (!path)
@@ -2004,40 +2023,6 @@ function fns( model ) {
2004
2023
  }
2005
2024
  }
2006
2025
 
2007
- function checkAssocOnSelf( ref, exprCtx, user ) {
2008
- // TODO: fully specify what `‹current_assoc›.‹backlink› = $self` means if the
2009
- // target of ‹backlink› is not the current entity.
2010
- // - what does it mean in an aspect
2011
- // - how about auto-redirections/rewrite
2012
- const { path } = ref;
2013
- if (path.length === 1 && path[0]._navigation?.kind === '$self') {
2014
- const query = userQuery( user );
2015
- const main = query?._main;
2016
- if (query && query !== main._leadingQuery) {
2017
- const { txt, op } = getQueryOperatorName( query );
2018
- const { location, id } = path[0];
2019
- error( 'ref-unexpected-self', [ location, user ], { '#': txt, id, op } );
2020
- }
2021
- return;
2022
- }
2023
- const index = userTargetElementPathIndex( user, path );
2024
- checkOnlyForeignKeyNavigation( user, path, index );
2025
- const target = index > 0 && index < path.length && ref._artifact?.target;
2026
- if (!target) {
2027
- const last = path[path.length - 1];
2028
- error( 'ref-expecting-target-assoc', [ last.location, user ], { id: '$self' },
2029
- 'Only an association of the target side can be compared to $(ID)' );
2030
- }
2031
- // in entity: target must match
2032
- // in aspect: must end with assoc, TODO: target must include aspect (+ add/ check)
2033
- else if (target._artifact && target._artifact !== user._main && user._main.kind === 'entity') {
2034
- const last = path[path.length - 1];
2035
- warning( 'ref-invalid-backlink', [ last.location, user ], { art: target, id: '$self' },
2036
- // eslint-disable-next-line @stylistic/max-len
2037
- 'The target $(ART) of the association is not the current entity represented by $(ID)' );
2038
- }
2039
- }
2040
-
2041
2026
 
2042
2027
  // right of union: parent = main → get correct operator
2043
2028
  // FROM subquery: parent = tab alias of outer query
@@ -2147,6 +2132,15 @@ function fns( model ) {
2147
2132
 
2148
2133
  // Low-level functions --------------------------------------------------------
2149
2134
 
2135
+ function proposeUsingDecl( name, sourceArtifacts ) {
2136
+ const art = model.definitions[name];
2137
+ // TODO: if there are sub artifacts, test whether at least one of them is real
2138
+ return art &&
2139
+ (art.kind !== 'namespace' || art._subArtifacts) &&
2140
+ !sourceArtifacts?.[name];
2141
+ ;
2142
+ }
2143
+
2150
2144
  /**
2151
2145
  * Make a "not found" error and optionally attach valid names.
2152
2146
  *