@sap/cds-compiler 6.4.2 → 6.5.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 (71) hide show
  1. package/CHANGELOG.md +87 -1159
  2. package/README.md +1 -10
  3. package/doc/IncompatibleChanges_v5.md +436 -0
  4. package/doc/IncompatibleChanges_v6.md +659 -0
  5. package/doc/Versioning.md +3 -7
  6. package/lib/api/main.js +1 -0
  7. package/lib/api/options.js +5 -0
  8. package/lib/api/validate.js +3 -0
  9. package/lib/base/message-registry.js +25 -2
  10. package/lib/base/messages.js +1 -1
  11. package/lib/base/model.js +3 -2
  12. package/lib/checks/actionsFunctions.js +6 -3
  13. package/lib/checks/existsExpressionsOnlyForeignKeys.js +16 -10
  14. package/lib/checks/existsInForbiddenPlaces.js +32 -0
  15. package/lib/checks/existsMustEndInAssoc.js +1 -1
  16. package/lib/checks/existsMustNotStartWithDollarSelf.js +31 -0
  17. package/lib/checks/validator.js +6 -2
  18. package/lib/compiler/assert-consistency.js +5 -7
  19. package/lib/compiler/builtins.js +5 -6
  20. package/lib/compiler/checks.js +4 -8
  21. package/lib/compiler/define.js +244 -459
  22. package/lib/compiler/extend.js +297 -11
  23. package/lib/compiler/finalize-parse-cdl.js +2 -10
  24. package/lib/compiler/generate.js +29 -1
  25. package/lib/compiler/populate.js +21 -63
  26. package/lib/compiler/propagator.js +1 -2
  27. package/lib/compiler/resolve.js +2 -12
  28. package/lib/compiler/shared.js +145 -114
  29. package/lib/compiler/tweak-assocs.js +14 -10
  30. package/lib/compiler/utils.js +97 -0
  31. package/lib/compiler/xpr-rewrite.js +113 -140
  32. package/lib/edm/annotations/edmJson.js +9 -6
  33. package/lib/edm/annotations/genericTranslation.js +8 -4
  34. package/lib/edm/csn2edm.js +3 -4
  35. package/lib/edm/edmInboundChecks.js +1 -2
  36. package/lib/edm/edmPreprocessor.js +3 -3
  37. package/lib/gen/CdlGrammar.checksum +1 -1
  38. package/lib/gen/CdlParser.js +4 -3
  39. package/lib/gen/Dictionary.json +16 -1
  40. package/lib/json/from-csn.js +4 -6
  41. package/lib/json/to-csn.js +3 -3
  42. package/lib/model/csnRefs.js +13 -4
  43. package/lib/model/enrichCsn.js +4 -2
  44. package/lib/optionProcessor.js +8 -4
  45. package/lib/parsers/AstBuildingParser.js +1 -1
  46. package/lib/render/utils/sql.js +3 -2
  47. package/lib/transform/db/applyTransformations.js +1 -1
  48. package/lib/transform/db/assertUnique.js +3 -3
  49. package/lib/transform/db/assocsToQueries/normalizeFrom.js +33 -0
  50. package/lib/transform/db/assocsToQueries/transformExists.js +17 -13
  51. package/lib/transform/db/assocsToQueries/utils.js +1 -6
  52. package/lib/transform/db/backlinks.js +4 -4
  53. package/lib/transform/db/cdsPersistence.js +4 -4
  54. package/lib/transform/db/constraints.js +4 -4
  55. package/lib/transform/db/expansion.js +5 -5
  56. package/lib/transform/db/flattening.js +4 -5
  57. package/lib/transform/db/rewriteCalculatedElements.js +3 -3
  58. package/lib/transform/db/temporal.js +11 -11
  59. package/lib/transform/draft/db.js +2 -0
  60. package/lib/transform/draft/odata.js +5 -7
  61. package/lib/transform/effective/flattening.js +1 -2
  62. package/lib/transform/forOdata.js +3 -3
  63. package/lib/transform/forRelationalDB.js +1 -1
  64. package/lib/transform/localized.js +13 -20
  65. package/lib/transform/odata/createForeignKeys.js +1 -2
  66. package/lib/transform/odata/flattening.js +1 -2
  67. package/lib/transform/odata/toFinalBaseType.js +52 -55
  68. package/lib/transform/transformUtils.js +3 -4
  69. package/package.json +3 -3
  70. package/doc/CHANGELOG_BETA.md +0 -464
  71. package/doc/CHANGELOG_DEPRECATED.md +0 -235
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ // The options are specified in ../optionProcessor.js (and some other files).
4
+ // Some backends feel the need to "translate" option, which require to list
5
+ // all options also here (as “public" or "private" option).
6
+
3
7
  const { validate, generateStringValidator } = require('./validate');
4
8
  const { makeMessageFunction } = require('../base/messages');
5
9
 
@@ -20,6 +24,7 @@ const publicOptionsNewAPI = [
20
24
  'defaultBinaryLength',
21
25
  'defaultStringLength',
22
26
  'csnFlavor',
27
+ 'noDollarCalc',
23
28
  // DB
24
29
  'sqlDialect',
25
30
  'sqlMapping',
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ // The options are specified in ../optionProcessor.js (and some other files).
4
+ // Options with non-boolean values must also be listed in this file.
5
+
3
6
  const { forEach } = require('../utils/objectUtils');
4
7
 
5
8
  /* eslint-disable arrow-body-style */
@@ -92,9 +92,16 @@ const centralMessages = {
92
92
  'ext-undefined-action': { severity: 'Warning' },
93
93
  'ext-undefined-art': { severity: 'Warning' }, // for annotate statement (for CDL path root)
94
94
  'ext-undefined-def': { severity: 'Warning' }, // for annotate statement (for CSN or CDL path cont)
95
+ 'ext-undefined-art-sec': { severity: 'Error', configurableFor: true }, // for security-relevant…
96
+ 'ext-undefined-def-sec': { severity: 'Error', configurableFor: true }, // … annotate statement
95
97
  'ext-undefined-element': { severity: 'Warning' },
96
98
  'ext-undefined-key': { severity: 'Warning' },
97
99
  'ext-undefined-param': { severity: 'Warning' },
100
+ 'ext-undefined-element-sec': { severity: 'Error', configurableFor: true }, // for security-relevant…
101
+ 'ext-undefined-action-sec': { severity: 'Error', configurableFor: true }, // for security-relevant…
102
+ 'ext-undefined-param-sec': { severity: 'Error', configurableFor: true }, // … annotate statement
103
+ 'ext-unexpected-returns': { severity: 'Warning' },
104
+ 'ext-unexpected-returns-sec': { severity: 'Error', configurableFor: true }, // … annotate statement
98
105
  'anno-unexpected-ellipsis': { severity: 'Error', configurableFor: 'deprecated' },
99
106
  'anno-unexpected-localized-skip': { severity: 'Error', configurableFor: true },
100
107
 
@@ -707,8 +714,8 @@ const centralMessageTexts = {
707
714
  on: 'Unexpected $(ID) reference; is valid only if compared to be equal to an association of the target side',
708
715
  subQuery: 'Unexpected $(ID) reference in a sub query',
709
716
  setQuery: 'Unexpected $(ID) reference in a query on the right side of $(OP)',
710
- exists: 'With $(NAME), path steps must not start with $(ID)',
711
- 'exists-filter': 'Unexpected $(ID) reference in filter of path $(ELEMREF) following “EXISTS” predicate',
717
+ exists: 'Paths following $(NAME) must not start with $(ID)',
718
+ 'exists-filter': 'Unexpected $(ID) reference in filter of assoc $(ELEMREF) following “EXISTS” predicate',
712
719
  },
713
720
  'ref-unexpected-map': {
714
721
  std: 'Unexpected reference to an element of type $(TYPE)', // unused
@@ -948,6 +955,8 @@ const centralMessageTexts = {
948
955
  'anno-builtin': 'Builtin types should not be annotated nor extended. Use custom type instead',
949
956
  'ext-undefined-def': 'Artifact $(ART) has not been found',
950
957
  'ext-undefined-art': 'No artifact has been found with name $(ART)',
958
+ 'ext-undefined-def-sec': 'Artifact $(ART) has not been found',
959
+ 'ext-undefined-art-sec': 'No artifact has been found with name $(ART)',
951
960
  'ext-undefined-element': {
952
961
  std: 'Element $(NAME) has not been found',
953
962
  element: 'Artifact $(ART) has no element $(NAME)',
@@ -955,15 +964,24 @@ const centralMessageTexts = {
955
964
  returns: 'Return value of $(ART) has no element $(NAME)',
956
965
  'enum-returns': 'Return value of $(ART) has no enum $(NAME)',
957
966
  },
967
+ 'ext-undefined-element-sec': 'Element $(NAME) has not been found',
958
968
  'ext-undefined-key': 'Foreign key $(NAME) has not been found',
959
969
  'ext-undefined-action': {
960
970
  std: 'Action $(ART) has not been found',
961
971
  action: 'Artifact $(ART) has no action $(NAME)',
962
972
  },
973
+ 'ext-undefined-action-sec': {
974
+ std: 'Action $(ART) has not been found',
975
+ action: 'Artifact $(ART) has no action $(NAME)',
976
+ },
963
977
  'ext-undefined-param': {
964
978
  std: 'Parameter $(ART) has not been found',
965
979
  param: 'Artifact $(ART) has no parameter $(NAME)',
966
980
  },
981
+ 'ext-undefined-param-sec': {
982
+ std: 'Parameter $(ART) has not been found',
983
+ param: 'Artifact $(ART) has no parameter $(NAME)',
984
+ },
967
985
 
968
986
  // annotation checks against their definition
969
987
  'anno-expecting-value': {
@@ -1157,6 +1175,11 @@ const centralMessageTexts = {
1157
1175
  redirected: 'Target $(TARGET) of $(NAME) is missing element $(ID); add an ON-condition to $(KEYWORD)',
1158
1176
  },
1159
1177
  'query-unexpected-assoc-hdbcds': 'Publishing a managed association in a view is not possible for “hdbcds” naming mode', // eslint-disable-line cds-compiler/message-no-quotes
1178
+ 'query-unexpected-exists': {
1179
+ std: 'Unexpected $(PROP) predicate',
1180
+ groupBy: 'Unexpected $(PROP) predicate in GROUP BY clause',
1181
+ orderBy: 'Unexpected $(PROP) predicate in ORDER BY clause',
1182
+ },
1160
1183
  'query-unexpected-structure-hdbcds': 'Publishing a structured element in a view is not possible for “hdbcds” naming mode', // eslint-disable-line cds-compiler/message-no-quotes
1161
1184
  'query-ignoring-param-nullability': {
1162
1185
  std: 'Ignoring nullability constraint on parameter when generating SAP HANA CDS view',
@@ -1724,7 +1724,7 @@ function constructSemanticLocationFromCsnPath( model, options, csnPath ) {
1724
1724
  else if (step === 'targetAspect') {
1725
1725
  // skip
1726
1726
  }
1727
- else if (step === 'xpr' || step === 'list' || step === 'default' || step === 'ref' || step === 'as' || step === 'value') {
1727
+ else if (step === 'xpr' || step === 'list' || step === 'default' || step === 'ref' || step === 'as' || step === 'value' || step === '$calc') {
1728
1728
  break; // don't go into xprs, refs, aliases, values, etc.
1729
1729
  }
1730
1730
  else if (step === 'returns') {
package/lib/base/model.js CHANGED
@@ -7,6 +7,9 @@
7
7
 
8
8
  const { forEach } = require('../utils/objectUtils');
9
9
 
10
+ // Normal options are in ../optionProcessor.js (and some other files),
11
+ // unfortunately partly non-grep-able (option `fooBar` is defined via `--foo-bar`)
12
+
10
13
  /**
11
14
  * Object of all available beta flags that will be enabled/disabled by `--beta-mode`
12
15
  * through cdsc. Only intended for INTERNAL USE.
@@ -29,8 +32,6 @@ const availableBetaFlags = {
29
32
  rewriteAnnotationExpressionsViaType: true,
30
33
  sqlServiceDummies: true,
31
34
  projectionViews: true,
32
- draftAdminDataHiddenFilter: true,
33
- $calcForDraft: true,
34
35
  // disabled by --beta-mode
35
36
  nestedServices: false,
36
37
  };
@@ -25,8 +25,9 @@ function checkActionOrFunction( art, artName, prop, path ) {
25
25
  for (const [ actName, act ] of Object.entries(art.actions)) {
26
26
  if (act.params) {
27
27
  checkExplicitBindingParameter.call(this, act.params, path.concat([ 'actions', actName, 'params' ]));
28
- for (const [ paramName, param ] of Object.entries(act.params))
29
- checkActionOrFunctionParameter.call(this, param, path.concat([ 'actions', actName, 'params', paramName ]), act.kind);
28
+ const params = Object.entries(act.params);
29
+ for (const [ paramName, param ] of params)
30
+ checkActionOrFunctionParameter.call(this, param, path.concat([ 'actions', actName, 'params', paramName ]), act.kind, params);
30
31
  }
31
32
  if (act.returns)
32
33
  checkReturns.bind(this)(act.returns, path.concat([ 'actions', actName, 'returns' ]), act.kind);
@@ -63,7 +64,9 @@ function checkActionOrFunction( art, artName, prop, path ) {
63
64
  * @param {CSN.Path} currPath path to the parameter
64
65
  * @param {string} actKind 'action' or 'function'
65
66
  */
66
- function checkActionOrFunctionParameter( param, currPath, actKind ) {
67
+ function checkActionOrFunctionParameter( param, currPath, actKind, params ) {
68
+ if ((param.items || param)?.type === '$self' && param === params?.[0][1])
69
+ return; // binding parameter
67
70
  const paramType = param.type ? this.csnUtils.getFinalTypeInfo(param.type) : param;
68
71
  if (!paramType)
69
72
  return; // no type could be resolved
@@ -3,39 +3,45 @@
3
3
  const { requireForeignKeyAccess } = require('../checks/onConditions');
4
4
 
5
5
  /**
6
- * Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
6
+ * Filter expressions in an exists path must:
7
+ * - only contain fk-accesses for assocs. Unmanaged traversal / non-fk access is forbidden.
8
+ * - not contain a ref starting with $self
7
9
  *
8
10
  * @param {CSN.Artifact} parent
9
11
  * @param {string} name
10
12
  * @param {Array} expr
11
13
  */
12
- function forbidAssocInExists( parent, name, expr ) {
14
+ function assertFilterOfExists( parent, name, expr ) {
13
15
  for (let i = 0; i < expr.length - 1; i++) {
14
16
  if (expr[i] === 'exists' && expr[i + 1].ref) {
15
17
  i++;
16
18
  const current = expr[i];
17
19
 
18
- const { _links } = expr[i];
20
+ const { _links } = current;
19
21
 
20
22
  const assocs = _links.filter(link => link.art?.target).map(link => current.ref[link.idx]);
21
23
 
22
- checkForInvalidAssoc.call(this, assocs);
24
+ ensureValidFilters.call(this, assocs);
23
25
  }
24
26
  }
25
27
  }
26
28
 
27
29
  /**
28
- * Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
30
+ * Reject:
31
+ * - Unmanaged traversal / non-fk access.
32
+ * - ref's starting with $self
29
33
  *
30
34
  * @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
31
35
  */
32
- function checkForInvalidAssoc( assocs ) {
36
+ function ensureValidFilters( assocs ) {
33
37
  for (const assoc of assocs) {
34
38
  if (assoc.where) {
35
39
  for (let i = 0; i < assoc.where.length; i++) {
36
40
  const part = assoc.where[i];
37
41
 
38
42
  if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
43
+ if (part.$scope === '$self')
44
+ this.error('ref-unexpected-self', part.$path, { '#': 'exists-filter', elemref: assoc.id, id: part.ref[0] });
39
45
  for (const link of part._links) {
40
46
  if (link.art && link.art.target) {
41
47
  if (link.art.keys) { // managed - allow FK access
@@ -55,7 +61,7 @@ function checkForInvalidAssoc( assocs ) {
55
61
  }
56
62
  // Recursively drill down if the assoc-step has a filter
57
63
  if (part.ref[link.idx].where)
58
- checkForInvalidAssoc.call(this, [ part.ref[link.idx] ]);
64
+ ensureValidFilters.call(this, [ part.ref[link.idx] ]);
59
65
  }
60
66
  }
61
67
  }
@@ -65,7 +71,7 @@ function checkForInvalidAssoc( assocs ) {
65
71
  }
66
72
 
67
73
  module.exports = {
68
- having: forbidAssocInExists,
69
- where: forbidAssocInExists,
70
- xpr: forbidAssocInExists,
74
+ having: assertFilterOfExists,
75
+ where: assertFilterOfExists,
76
+ xpr: assertFilterOfExists,
71
77
  };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { applyTransformationsOnNonDictionary } = require('../model/csnUtils');
4
+
5
+ /**
6
+ * Checks a query for forbidden usage of the `exists` predicate within `groupBy` and `orderBy`.
7
+ *
8
+ * @param {object} query
9
+ * @param {string} prop - either groupBy or orderBy.
10
+ * @param {object[]} _tokenStream the value of query[prop]
11
+ * @param {object|Array} pathToClause - Path to respective groupBy/orderBy clause in the CSN.
12
+ * @this {object} The calling context must provide an `error` function.
13
+ */
14
+ function existsInForbiddenPlaces( query, prop, _tokenStream, pathToClause ) {
15
+ applyTransformationsOnNonDictionary(query, prop, {
16
+ ref: (_parent, _prop, _ref, pathToError, tokenStream, index) => {
17
+ if (tokenStream[index - 1] === 'exists') {
18
+ // the path should point to the exists
19
+ const pathToExists = pathToError.with(-1, index - 1);
20
+ this.error('query-unexpected-exists', pathToExists, {
21
+ '#': prop,
22
+ prop: 'exists',
23
+ });
24
+ }
25
+ },
26
+ }, null, pathToClause);
27
+ }
28
+
29
+ module.exports = {
30
+ groupBy: existsInForbiddenPlaces,
31
+ orderBy: existsInForbiddenPlaces,
32
+ };
@@ -13,8 +13,8 @@ function existsMustEndInAssoc( parent, prop, expression, path ) {
13
13
  if (expression[i] === 'exists') {
14
14
  const next = expression[i + 1];
15
15
  const { _art } = next;
16
- const errorPath = path.concat([ prop, i ]);
17
16
  if (!next.SELECT && !_art?.target) {
17
+ const errorPath = path.concat([ prop, i ]);
18
18
  this.error('ref-expecting-assoc', errorPath, {
19
19
  '#': _art.type ? 'with-type' : 'std',
20
20
  elemref: next,
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A path following an “exists” predicate must always end in an association.
5
+ *
6
+ * @param {object} parent
7
+ * @param {string} prop
8
+ * @param {Array} expression
9
+ * @param {CSN.Path} path
10
+ */
11
+ function existsMustNotStartWithDollarSelf( parent, prop, expression, path ) {
12
+ for (let i = 0; i < expression?.length - 1; i++) {
13
+ if (expression[i] === 'exists') {
14
+ const next = expression[i + 1];
15
+ if (next.$scope === '$self') {
16
+ const errorPath = path.concat([ prop, i ]);
17
+ this.error('ref-unexpected-self', errorPath, {
18
+ '#': 'exists',
19
+ id: next.ref[0],
20
+ name: 'exists',
21
+ });
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ module.exports = {
28
+ having: existsMustNotStartWithDollarSelf,
29
+ where: existsMustNotStartWithDollarSelf,
30
+ xpr: existsMustNotStartWithDollarSelf,
31
+ };
@@ -43,7 +43,9 @@ const { validateAssociationsInItems } = require('./arrayOfs');
43
43
  const checkQueryForNoDBArtifacts = require('./queryNoDbArtifacts');
44
44
  const checkExplicitlyNullableKeys = require('./nullableKeys');
45
45
  const existsMustEndInAssoc = require('./existsMustEndInAssoc');
46
- const forbidAssocInExists = require('./existsExpressionsOnlyForeignKeys');
46
+ const existsMustNotStartWithDollarSelf = require('./existsMustNotStartWithDollarSelf');
47
+ const existsInForbiddenPlaces = require('./existsInForbiddenPlaces');
48
+ const assertFilterOfExists = require('./existsExpressionsOnlyForeignKeys');
47
49
  const checkPathsInStoredCalcElement = require('./checkPathsInStoredCalcElement');
48
50
  const managedWithoutKeys = require('./managedWithoutKeys');
49
51
  const {
@@ -84,8 +86,10 @@ const forRelationalDBArtifactValidators = [
84
86
 
85
87
  const forRelationalDBCsnValidators = [
86
88
  checkCdsMap,
89
+ existsInForbiddenPlaces,
87
90
  existsMustEndInAssoc,
88
- forbidAssocInExists,
91
+ existsMustNotStartWithDollarSelf,
92
+ assertFilterOfExists,
89
93
  navigationIntoMany,
90
94
  checkPathsInStoredCalcElement,
91
95
  featureFlags,
@@ -207,7 +207,7 @@ function assertConsistency( model, stage ) {
207
207
  'elements', '$autoElement', '$uncheckedElements', '_origin', '_extensions',
208
208
  '$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps',
209
209
  '$calcDepElement', '$filtered', '$enclosed', '_parent',
210
- 'deprecated', '$onlyInExprCtx',
210
+ 'deprecated', '$restricted',
211
211
  ],
212
212
  schema: {
213
213
  kind: { test: isString, enum: [ 'builtin' ] },
@@ -216,7 +216,7 @@ function assertConsistency( model, stage ) {
216
216
  $uncheckedElements: { test: isBoolean },
217
217
  $requireElementAccess: { test: isBoolean },
218
218
  deprecated: { test: isBoolean },
219
- $onlyInExprCtx: { test: TODO },
219
+ $restricted: { test: TODO },
220
220
  // missing location for normal "elements"
221
221
  elements: { test: TODO },
222
222
  },
@@ -316,7 +316,7 @@ function assertConsistency( model, stage ) {
316
316
  requires: [ 'location', 'path' ],
317
317
  optional: [
318
318
  'kind', 'name', '$syntax', '_block', '_parent', '_main',
319
- 'elements', '_origin', '_joinParent', '$joinArgsIndex', '$syntax',
319
+ 'elements', '_origin', '_joinParent', '$joinArgsIndex',
320
320
  '$parens', '_status', // TODO: only in from
321
321
  'scope', '_artifact', '_originalArtifact', '$inferred', 'kind',
322
322
  '_effectiveType', '$effectiveSeqNo', // TODO:check this
@@ -449,7 +449,7 @@ function assertConsistency( model, stage ) {
449
449
  requires: [ 'location', 'path' ],
450
450
  optional: [
451
451
  'scope', 'variant', '_artifact', '_originalArtifact',
452
- '$inferred', '$parens', 'sort', 'nulls',
452
+ '$inferred', '$parens', 'sort', 'nulls', '$syntax',
453
453
  ],
454
454
  },
455
455
  none: { optional: () => true }, // parse error
@@ -507,7 +507,7 @@ function assertConsistency( model, stage ) {
507
507
  args: {
508
508
  inherits: 'value',
509
509
  optional: [
510
- 'name', '$duplicate', '$expected', 'args', 'suffix', '$parens',
510
+ 'name', '$duplicate', 'args', 'suffix', '$parens',
511
511
  'param', 'scope', // for dynamic parameter '?'
512
512
  ],
513
513
  test: args,
@@ -724,7 +724,6 @@ function assertConsistency( model, stage ) {
724
724
  'localized-entity', // `.texts` entity
725
725
  'localized-origin', // `.texts` entity
726
726
  'nav', // only used for MASKED, TODO(v6): Remove
727
- 'none', // only used in ensureColumnName(): Used in object representing empty alias
728
727
  'query', // inferred query properties, e.g. `key`
729
728
  'rewrite', // on-conditions or FKeys are rewritten
730
729
  'parent-origin', // annotation/property copied from parent that does not come through
@@ -747,7 +746,6 @@ function assertConsistency( model, stage ) {
747
746
  $withLocalized: { test: isBoolean },
748
747
  $sources: { parser: true, test: isArray( isString ) },
749
748
  tokenStream: { parser: true, test: TODO },
750
- $expected: { parser: true, test: isOneOf( [ 'approved-exists', 'exists' ] ) },
751
749
  $messageFunctions: { test: TODO },
752
750
  $functions: { test: TODO },
753
751
  $assert: { test: TODO }, // currently just for missing Error[ref-cycle]
@@ -211,8 +211,7 @@ const magicVariables = {
211
211
  },
212
212
  // Require that elements are accessed, i.e. no $draft, only $draft.<element>.
213
213
  $requireElementAccess: true,
214
- // See reference semantics in shared.js
215
- $onlyInExprCtx: [ 'annotation', 'annoRewrite' ],
214
+ $restricted: true, // only in annotation expression, see shared.js
216
215
  },
217
216
  };
218
217
 
@@ -454,8 +453,8 @@ function initBuiltins( model ) {
454
453
  art.$requireElementAccess = magic.$requireElementAccess;
455
454
  if (magic.deprecated)
456
455
  art.deprecated = magic.deprecated;
457
- if (magic.$onlyInExprCtx)
458
- art.$onlyInExprCtx = magic.$onlyInExprCtx;
456
+ if (magic.$restricted)
457
+ art.$restricted = magic.$restricted;
459
458
 
460
459
  createMagicElements( art, magic.elements );
461
460
  if (options.variableReplacements?.[id])
@@ -480,8 +479,8 @@ function initBuiltins( model ) {
480
479
  // Propagate this property so that it is available for sub-elements.
481
480
  if (art.$uncheckedElements)
482
481
  magic.$uncheckedElements = art.$uncheckedElements;
483
- if (art.$onlyInExprCtx)
484
- magic.$onlyInExprCtx = art.$onlyInExprCtx;
482
+ if (art.$restricted)
483
+ magic.$restricted = art.$restricted;
485
484
  setProp( magic, '_parent', art );
486
485
  // setProp( magic, '_effectiveType', magic );
487
486
  if (elements[id] && typeof elements[id] === 'object')
@@ -941,14 +941,8 @@ function check( model ) {
941
941
  */
942
942
  function checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail ) {
943
943
  // Arg must not be an association and not $self
944
- // Only if path is not approved exists path (that is non-query position)
945
- if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
946
- if (arg.$expected === 'exists') {
947
- const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
948
- error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
949
- }
950
- }
951
- else if (rejectAssocTail && isAssociationOperand( arg )) {
944
+ // Only if path is not after an `exists`
945
+ if (arg.$syntax !== 'after-exists' && rejectAssocTail && isAssociationOperand( arg )) {
952
946
  if (rejectAssocTail.rejectManaged && rejectAssocTail.rejectUnmanaged ||
953
947
  rejectAssocTail.rejectManaged && arg._artifact.keys ||
954
948
  rejectAssocTail.rejectUnmanaged && arg._artifact.on) {
@@ -956,6 +950,8 @@ function check( model ) {
956
950
  const context = rejectAssocTail.context === 'query' && 'query-' ||
957
951
  rejectAssocTail.context === 'anno' && 'anno-' ||
958
952
  '';
953
+ // Hm, in other message context about associations or compositions, we do
954
+ // not have separate text variants for association or composition…
959
955
  const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
960
956
  error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': `${ context }${ variant }` } );
961
957
  }