@sap/cds-compiler 3.6.2 → 3.8.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 (89) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +3 -0
  3. package/bin/cdsc.js +12 -5
  4. package/doc/CHANGELOG_ARCHIVE.md +6 -6
  5. package/doc/CHANGELOG_BETA.md +35 -2
  6. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  7. package/doc/DeprecatedOptions_v2.md +1 -1
  8. package/doc/NameResolution.md +1 -1
  9. package/lib/api/main.js +63 -23
  10. package/lib/api/options.js +1 -0
  11. package/lib/api/validate.js +5 -0
  12. package/lib/base/dictionaries.js +15 -3
  13. package/lib/base/keywords.js +2 -0
  14. package/lib/base/message-registry.js +120 -34
  15. package/lib/base/messages.js +51 -27
  16. package/lib/base/model.js +4 -2
  17. package/lib/base/shuffle.js +2 -1
  18. package/lib/checks/arrayOfs.js +1 -1
  19. package/lib/checks/defaultValues.js +1 -1
  20. package/lib/checks/elements.js +29 -1
  21. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +10 -6
  22. package/lib/checks/invalidTarget.js +1 -1
  23. package/lib/checks/nonexpandableStructured.js +1 -1
  24. package/lib/checks/onConditions.js +15 -9
  25. package/lib/checks/sql-snippets.js +2 -2
  26. package/lib/checks/types.js +5 -1
  27. package/lib/checks/validator.js +7 -3
  28. package/lib/compiler/assert-consistency.js +42 -26
  29. package/lib/compiler/base.js +50 -4
  30. package/lib/compiler/builtins.js +17 -8
  31. package/lib/compiler/checks.js +241 -246
  32. package/lib/compiler/define.js +113 -146
  33. package/lib/compiler/extend.js +889 -383
  34. package/lib/compiler/finalize-parse-cdl.js +5 -58
  35. package/lib/compiler/index.js +1 -1
  36. package/lib/compiler/kick-start.js +7 -8
  37. package/lib/compiler/populate.js +297 -293
  38. package/lib/compiler/propagator.js +27 -18
  39. package/lib/compiler/resolve.js +146 -463
  40. package/lib/compiler/shared.js +36 -79
  41. package/lib/compiler/tweak-assocs.js +30 -28
  42. package/lib/compiler/utils.js +31 -5
  43. package/lib/edm/annotations/genericTranslation.js +131 -59
  44. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  45. package/lib/edm/csn2edm.js +22 -5
  46. package/lib/edm/edm.js +6 -4
  47. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  48. package/lib/edm/edmPreprocessor.js +42 -26
  49. package/lib/gen/Dictionary.json +38 -2
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +3 -1
  52. package/lib/gen/languageLexer.js +1 -1
  53. package/lib/gen/languageParser.js +4828 -4472
  54. package/lib/inspect/inspectPropagation.js +20 -34
  55. package/lib/json/from-csn.js +140 -44
  56. package/lib/json/to-csn.js +114 -122
  57. package/lib/language/errorStrategy.js +2 -0
  58. package/lib/language/genericAntlrParser.js +156 -36
  59. package/lib/language/language.g4 +100 -58
  60. package/lib/language/textUtils.js +13 -0
  61. package/lib/main.d.ts +43 -3
  62. package/lib/main.js +4 -2
  63. package/lib/model/csnRefs.js +15 -3
  64. package/lib/model/csnUtils.js +12 -74
  65. package/lib/model/revealInternalProperties.js +4 -2
  66. package/lib/modelCompare/compare.js +2 -1
  67. package/lib/optionProcessor.js +3 -0
  68. package/lib/render/manageConstraints.js +5 -2
  69. package/lib/render/toCdl.js +216 -104
  70. package/lib/render/toHdbcds.js +2 -9
  71. package/lib/render/toRename.js +14 -51
  72. package/lib/render/toSql.js +4 -3
  73. package/lib/render/utils/common.js +9 -5
  74. package/lib/transform/braceExpression.js +6 -0
  75. package/lib/transform/db/assertUnique.js +2 -1
  76. package/lib/transform/db/expansion.js +2 -0
  77. package/lib/transform/db/flattening.js +37 -36
  78. package/lib/transform/db/rewriteCalculatedElements.js +600 -0
  79. package/lib/transform/db/transformExists.js +4 -0
  80. package/lib/transform/db/views.js +40 -37
  81. package/lib/transform/forOdataNew.js +20 -15
  82. package/lib/transform/forRelationalDB.js +58 -41
  83. package/lib/transform/odata/typesExposure.js +50 -15
  84. package/lib/transform/parseExpr.js +16 -8
  85. package/lib/transform/transformUtilsNew.js +42 -14
  86. package/lib/transform/translateAssocsToJoins.js +60 -37
  87. package/lib/transform/universalCsn/coreComputed.js +15 -7
  88. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  89. package/package.json +2 -1
@@ -4,7 +4,13 @@ const { createMessageFunctions } = require('../base/messages');
4
4
  const { locationString } = require('../base/location');
5
5
  const { findArtifact, stringRefToPath } = require('./inspectUtils');
6
6
  const { term } = require('../utils/term');
7
- const { CompilerAssertion } = require('../base/error');
7
+
8
+ const inferredNiceOutput = {
9
+ '*': 'wildcard',
10
+ 'aspect-composition': 'composition',
11
+ prop: 'propagation',
12
+ $generated: 'generated',
13
+ };
8
14
 
9
15
  /**
10
16
  * @param {XSN.Model} xsn
@@ -77,29 +83,16 @@ function _inspectAnnotations( artifactXsn ) {
77
83
  for (const anno of annos) {
78
84
  const annoXsn = artifactXsn[anno];
79
85
  const loc = locationString(annoXsn.name.location);
86
+
80
87
  let origin;
81
- switch (annoXsn.$priority) {
82
- case false:
83
- if (annoXsn.$inferred)
84
- origin = 'propagation';
85
- else
86
- origin = 'direct';
87
- break;
88
-
89
- case 'extend':
90
- case 'annotate':
91
- origin = annoXsn.$priority;
92
- break;
93
-
94
- case undefined:
95
- if (annoXsn.$inferred === '$generated') {
96
- origin = 'generated';
97
- break;
98
- }
99
- // fallthrough
100
- default:
101
- throw new CompilerAssertion(`inspect anno: Unhandled Case: ${ annoXsn.$priority }`);
102
- }
88
+ if (annoXsn.$inferred === '$generated')
89
+ origin = 'generated';
90
+ else if (annoXsn.$inferred)
91
+ origin = inferredNiceOutput[annoXsn.$inferred] || annoXsn.$inferred;
92
+ else if (isContainedInParentLocation(annoXsn.name, artifactXsn))
93
+ origin = 'direct';
94
+ else
95
+ origin = 'annotate'; // ...or `extend`
103
96
 
104
97
  maxAnnoLength = Math.max(maxAnnoLength, anno.length);
105
98
 
@@ -122,11 +115,6 @@ function _inspectElements( artifactXsn ) {
122
115
  const result = [];
123
116
  const elements = Object.keys(artifactXsn.elements);
124
117
 
125
- const inferredNiceOutput = {
126
- '*': 'wildcard',
127
- 'aspect-composition': 'composition',
128
- };
129
-
130
118
  let maxElemLength = 12;
131
119
  let maxOriginLength = 6;
132
120
 
@@ -153,7 +141,7 @@ function _inspectElements( artifactXsn ) {
153
141
  origin = elementXsn.$inferred;
154
142
  }
155
143
  else if (!isContainedInParentLocation(elementXsn, artifactXsn)) {
156
- // just a heuristic
144
+ // just a heuristic - a good enough one
157
145
  origin = 'extend';
158
146
  }
159
147
  else {
@@ -192,11 +180,9 @@ function isContainedInParentLocation( art, parent ) {
192
180
  const parentLoc = parent.location;
193
181
  if (artLoc.file !== parentLoc.file)
194
182
  return false;
195
- if (artLoc.line < parentLoc.line || artLoc.line > parentLoc.endLine)
196
- return false;
197
- // Good enough for now
198
- // TODO: Check columns
199
- return true;
183
+ const startDiff = artLoc.line - parentLoc.line || artLoc.col - parentLoc.col;
184
+ const endDiff = artLoc.endLine - parentLoc.endLine || artLoc.endCol - parentLoc.endCol;
185
+ return startDiff >= 0 && endDiff <= 0;
200
186
  }
201
187
 
202
188
  module.exports = {
@@ -62,8 +62,8 @@
62
62
  * @property {number} [minLength] Minimum number of elements that an array must have.
63
63
  * @property {boolean} [inValue] Puts the value into an XSN property "value",
64
64
  * e.g. { value: ... }
65
- * @property {string} [xorGroup] Corresponding xor group. It references a value of
66
- * xorGroups. If set then only one property of of the
65
+ * @property {string[]} [xorGroups] Corresponding xor groups. It references a value of
66
+ * xorGroups. If set then only one property of the
67
67
  * xorGroup may be set, e.g. if target is set, elements
68
68
  * may not.
69
69
  * @property {string} [xsnOp] Defines the operator to be used for XSN. Used for SET
@@ -75,6 +75,8 @@
75
75
  * it.
76
76
  * @property {string} [xorException] A property name that is allowed besides another property
77
77
  * of an xorGroup (as an exception to the rule).
78
+ * @property {boolean} [ignoreExtra] Whether extra properties are ignored and not put
79
+ * into $extra.
78
80
  */
79
81
 
80
82
  /**
@@ -90,6 +92,7 @@
90
92
  const { dictAdd } = require('../base/dictionaries');
91
93
  const { quotedLiteralPatterns } = require('../compiler/builtins');
92
94
  const { CompilerAssertion } = require('../base/error');
95
+ const { isBetaEnabled } = require('../base/model');
93
96
 
94
97
  const $location = Symbol.for('cds.$location');
95
98
 
@@ -121,7 +124,8 @@ const xorGroups = {
121
124
  ':expr': [ // see also xorException property in schema
122
125
  'ref', 'xpr', 'list', 'val', '#', 'func', 'SELECT', 'SET', 'expand',
123
126
  '=', 'path', 'value', 'op', // '='/'path' is CSN v0.1.0 here
124
- ], // also set xorGroupTwo ':col' for 'expand' and 'inline'
127
+ ],
128
+ ':col': [ 'expand', 'inline' ],
125
129
  ':ext': [ 'annotate', 'extend' ], // TODO: better msg for test/negative/UnexpectedProperties.csn
126
130
  ':assoc': [ 'on', 'keys', 'foreignKeys', 'onCond' ], // 'foreignKeys'/'onCond' is CSN v0.1.0
127
131
  // TODO - improve consequential errors: assume no name given with `join` or `inline`?
@@ -162,10 +166,15 @@ const schemaClasses = {
162
166
  xpr: {
163
167
  class: 'condition',
164
168
  type: xprInValue,
165
- xorException: 'func', // see xorGroup :expr
169
+ xorException: 'func', // see xorGroup :expr; for window functions
166
170
  inKind: [ '$column' ],
167
171
  inValue: true,
168
172
  },
173
+ '=': {
174
+ // by not setting `vZeroFor`, we disallow `=` in `columns`.
175
+ // CSN v0.1 didn't have columns, so this isn't breaking v0.1 compatibility.
176
+ type: ignore,
177
+ },
169
178
  },
170
179
  },
171
180
  };
@@ -252,12 +261,10 @@ const schema = compileSchema( {
252
261
  expand: {
253
262
  class: 'columns',
254
263
  xorException: 'ref', // see xorGroup :expr
255
- xorGroupTwo: ':col',
256
264
  inKind: [ '$column' ], // only valid in $column
257
265
  },
258
266
  inline: {
259
267
  class: 'columns',
260
- xorGroupTwo: ':col',
261
268
  onlyWith: 'ref',
262
269
  inKind: [ '$column' ], // only valid in $column
263
270
  },
@@ -298,10 +305,20 @@ const schema = compileSchema( {
298
305
  },
299
306
  // type properties (except: elements, enum, keys, on): ---------------------
300
307
  type: {
301
- type: artifactRef,
308
+ type: typeArtifactRef,
302
309
  msgVariant: 'or-object', // for 'syntax-expecting-string',
303
310
  optional: [ 'ref', 'global' ],
304
311
  inKind: [ 'element', 'type', 'param', 'mixin', 'event', 'annotation', 'extend' ],
312
+ schema: {
313
+ ref: {
314
+ arrayOf: typeRefItem,
315
+ type: renameTo( 'path', typeRef ),
316
+ minLength: 1,
317
+ requires: 'id',
318
+ optional: [ 'id' ],
319
+ ignoreExtra: true, // custom properties inside `ref` ignored.
320
+ },
321
+ },
305
322
  },
306
323
  targetAspect: {
307
324
  type: artifactRef,
@@ -424,6 +441,7 @@ const schema = compileSchema( {
424
441
  list: {
425
442
  class: 'condition',
426
443
  type: list,
444
+ inKind: [ '$column' ],
427
445
  },
428
446
  val: {
429
447
  type: value,
@@ -592,6 +610,19 @@ const schema = compileSchema( {
592
610
  prop: '@‹anno›', // which property name do messages use for annotation assignments?
593
611
  type: annotation,
594
612
  inKind: () => true, // allowed in all definitions (including columns and extensions)
613
+ schema: {
614
+ '-expr': { // '-expr' and '-' must not exist top-level
615
+ prop: '@‹anno›',
616
+ type: object,
617
+ optional: [ '=', '#', 'xpr', 'ref', 'val', 'list', 'literal', 'func', 'args' ],
618
+ schema: {
619
+ '=': {
620
+ type: renameTo( '$tokenTexts', string ),
621
+ xorGroups: null, // reset xorGroup; allow '=' for all :expr
622
+ },
623
+ },
624
+ },
625
+ },
595
626
  },
596
627
  abstract: { // v1: with 'abstract', an entity becomes an aspect
597
628
  type: abstract,
@@ -607,7 +638,7 @@ const schema = compileSchema( {
607
638
  },
608
639
  notNull: {
609
640
  type: boolOrNull,
610
- inKind: [ 'element', 'param' ], // TODO: $column - or if so: in 'cast'?
641
+ inKind: [ 'element', 'param', 'type' ], // TODO: $column - or if so: in 'cast'?
611
642
  },
612
643
  virtual: {
613
644
  type: boolOrNull,
@@ -620,7 +651,7 @@ const schema = compileSchema( {
620
651
  // 2. Inside "xpr" => inside expressions
621
652
  // Because of (1) we have to set this property to false.
622
653
  inValue: false,
623
- optional: typeProperties,
654
+ optional: typeProperties, // TODO: only in CDL-style cast, otherwise just length,…
624
655
  inKind: [ '$column' ],
625
656
  },
626
657
  default: {
@@ -731,6 +762,8 @@ let virtualLine = 1;
731
762
  /** @type {CSN.Location[]} */
732
763
  let dollarLocations = [];
733
764
  let arrayLevelCount = 0;
765
+ /** @type {CSN.Options} */
766
+ let userOptions = null; // must be reset!
734
767
 
735
768
  /**
736
769
  * @param {Object.<string, SchemaSpec>} specs
@@ -763,12 +796,15 @@ function compileSchema( specs, proto = null ) {
763
796
  else
764
797
  throw new CompilerAssertion( `Missing type specification for property "${ p }"` );
765
798
  }
766
- }
767
- // Set property 'xorGroup' in main and sub schema:
768
- for (const group in xorGroups) {
769
- for (const prop of xorGroups[group]) {
770
- if (r[prop].xorGroup === undefined)
771
- r[prop].xorGroup = group;
799
+
800
+ if (s.xorGroups === undefined) {
801
+ // Only set xorGroup once. Could already be set through shared sub-schema
802
+ // of schemaClasses or be explicitly set.
803
+ s.xorGroups = [];
804
+ for (const group in xorGroups) {
805
+ if (xorGroups[group].includes(p))
806
+ s.xorGroups.push(group);
807
+ }
772
808
  }
773
809
  }
774
810
  if (proto)
@@ -1110,6 +1146,14 @@ function validKind( val, spec, xsn ) {
1110
1146
  return ignore( val );
1111
1147
  }
1112
1148
 
1149
+ function typeArtifactRef( ref, spec ) {
1150
+ if (ref && typeof ref === 'object' && !Array.isArray( ref )) {
1151
+ if (ref.ref?.length === 1)
1152
+ return artifactRef( ref, { ...spec, ignoreExtra: true } );
1153
+ }
1154
+ return artifactRef( ref, spec );
1155
+ }
1156
+
1113
1157
  // Use with spec.msgVariant: 'or-object'
1114
1158
  function artifactRef( ref, spec ) {
1115
1159
  if (!ref || typeof ref !== 'string') {
@@ -1230,6 +1274,32 @@ function symbol( id, spec, xsn ) { // for CSN property '#'
1230
1274
  xsn.sym = { id, location: location() };
1231
1275
  }
1232
1276
 
1277
+ /**
1278
+ * Wrapper around the default `ref` spec: Don't allow references of length 1 for types.
1279
+ */
1280
+ function typeRef( val, spec, xsn, csn ) {
1281
+ // e.g. { ref: [ 'T' ] }
1282
+ if (Array.isArray(val) && val.length <= 1)
1283
+ warning( 'syntax-deprecated-type-ref', location(true), { '#': 'std', prop: 'type' });
1284
+
1285
+ return arrayOf(spec.arrayOf)(val, spec, xsn, csn);
1286
+ }
1287
+
1288
+ /**
1289
+ * Similar to refItem(), but warns that the item should be a string if `id` is the only CSN
1290
+ * property inside the ref-item.
1291
+ */
1292
+ function typeRefItem( val, spec, xsn, csn ) {
1293
+ // e.g. [ 'T', { id: 'elem', other_prop: true } ]
1294
+ // avoid duplicate messages for single-item reference, see typeRef()
1295
+ if (val && csn.ref?.length > 1 && typeof val === 'object' && val.id) {
1296
+ const ownKeysCount = Object.keys(val).filter(key => ourpropsRegex.test(key)).length;
1297
+ if (ownKeysCount === 1)
1298
+ warning('syntax-deprecated-type-ref', location(true), { '#': 'ref-item', prop: 'ref[]' });
1299
+ }
1300
+ return refItem(val, spec);
1301
+ }
1302
+
1233
1303
  /**
1234
1304
  * returns:
1235
1305
  * - false = no "...",
@@ -1283,21 +1353,28 @@ function annoValue( val, spec ) {
1283
1353
  }
1284
1354
  return retval;
1285
1355
  }
1286
- if (typeof val['#'] === 'string') {
1287
- if (Object.keys( val ).length === 1) {
1356
+ else if (typeof val['='] === 'string') {
1357
+ const valKeys = Object.keys(val);
1358
+ if (valKeys.length > 1 && isBetaEnabled(userOptions, 'annotationExpressions')) {
1359
+ const s = schema['@'].schema['-expr'];
1360
+ const r = { location: location() };
1361
+ Object.assign(r, object(val, s));
1362
+ return r;
1363
+ }
1364
+ else if (valKeys.length === 1) {
1288
1365
  ++virtualLine;
1289
- const xsn = { location: location() };
1290
- symbol( val['#'], schema['#'], xsn );
1366
+ const r = refSplit( val['='], '=' );
1291
1367
  ++virtualLine;
1292
- return xsn;
1368
+ return r;
1293
1369
  }
1294
1370
  }
1295
- else if (typeof val['='] === 'string') {
1371
+ if (typeof val['#'] === 'string') {
1296
1372
  if (Object.keys( val ).length === 1) {
1297
1373
  ++virtualLine;
1298
- const r = refSplit( val['='], '=' );
1374
+ const xsn = { location: location() };
1375
+ symbol( val['#'], schema['#'], xsn );
1299
1376
  ++virtualLine;
1300
- return r;
1377
+ return xsn;
1301
1378
  }
1302
1379
  }
1303
1380
  else if (val['...'] && Object.keys( val ).length === 1) {
@@ -1314,13 +1391,13 @@ function annoValue( val, spec ) {
1314
1391
  ++virtualLine;
1315
1392
  return r;
1316
1393
  }
1317
- const struct = Object.create(null);
1394
+ const r = { struct: Object.create(null), literal: 'struct', location: location() };
1318
1395
  ++virtualLine;
1319
1396
  for (const name of Object.keys( val )) {
1320
- struct[name] = annotation( val[name], schema['@'], null, val, name );
1397
+ r.struct[name] = annotation( val[name], schema['@'], null, val, name );
1321
1398
  ++virtualLine;
1322
1399
  }
1323
- return { struct, literal: 'struct', location: location() };
1400
+ return r;
1324
1401
  }
1325
1402
 
1326
1403
  function annotation( val, spec, xsn, csn, name ) {
@@ -1333,6 +1410,7 @@ function annotation( val, spec, xsn, csn, name ) {
1333
1410
  n.absolute = absolute;
1334
1411
  if (variantIndex < absolute.length)
1335
1412
  n.variant = { id: name.substring( variantIndex ), location: location() };
1413
+
1336
1414
  const r = annoValue( val, spec );
1337
1415
  r.name = n;
1338
1416
  return r;
@@ -1612,8 +1690,11 @@ function getSpec( parentSpec, csn, prop, xor, expected, kind ) {
1612
1690
  const p0 = schema[prop] ? prop : prop.charAt(0);
1613
1691
  const s = (parentSpec.schema || schema)[p0];
1614
1692
  if (!s || s.noPrefix === (prop !== p0) ) {
1615
- if (prop && !ourpropsRegex.test( prop ))
1693
+ if (prop && !ourpropsRegex.test( prop )) {
1694
+ if (parentSpec.ignoreExtra)
1695
+ return { prop, type: ignore };
1616
1696
  return { prop, type: extra };
1697
+ }
1617
1698
  // TODO v4: No warning with --sloppy
1618
1699
  warning( 'syntax-unknown-property', location(true), { prop },
1619
1700
  'Unknown CSN property $(PROP)' );
@@ -1628,10 +1709,10 @@ function getSpec( parentSpec, csn, prop, xor, expected, kind ) {
1628
1709
  }
1629
1710
  const zero = s.vZeroFor;
1630
1711
  if (zero) { // (potential) CSN v0.1.0 property
1631
- const group = s.xorGroup;
1632
- if (expected( zero, schema[zero] ) && !(group && xor[group])) {
1712
+ const groups = s.xorGroups;
1713
+ if (expected( zero, schema[zero] ) && !(groups.length && groups.every(group => xor[group]))) {
1633
1714
  replaceZeroProp( prop, zero );
1634
- if (group)
1715
+ for (const group of groups)
1635
1716
  xor[group] = prop;
1636
1717
  onlyWith( s, s.onlyWith, csn, prop, xor, expected );
1637
1718
  return s;
@@ -1650,8 +1731,8 @@ function getSpec( parentSpec, csn, prop, xor, expected, kind ) {
1650
1731
  kind,
1651
1732
  } );
1652
1733
  }
1653
- else if (checkAndSetXorGroup( s.xorGroup, s.xorException, prop, xor ) &&
1654
- checkAndSetXorGroup( s.xorGroupTwo, s.xorException, prop, xor ) ) {
1734
+ else if (checkAndSetXorGroup( s.xorGroups, s.xorException, prop, xor )) {
1735
+ // TODO: If all targets of onlyWith are xor-excluded/ignore, also exclude/ignore this one.
1655
1736
  onlyWith( s, s.onlyWith, csn, prop, xor, expected );
1656
1737
  return s;
1657
1738
  }
@@ -1693,7 +1774,7 @@ function onlyWith( spec, need, csn, prop, xor, expected ) {
1693
1774
  const allowed = need.filter( p => expected( p, spec ));
1694
1775
  // There should be at least one elem, otherwise the spec is wrong;
1695
1776
  // first try to find element which is not excluded
1696
- need = allowed.find( p => !xor[schema[p].xorGroup] ) || allowed[0];
1777
+ need = allowed.find( p => !schema[p].xorGroups?.some(g => xor[g]) ) || allowed[0];
1697
1778
  }
1698
1779
  if (prop) {
1699
1780
  error( 'syntax-missing-property', location(true), // location at $(PROP)
@@ -1713,18 +1794,31 @@ function onlyWith( spec, need, csn, prop, xor, expected ) {
1713
1794
  return spec;
1714
1795
  }
1715
1796
 
1716
- function checkAndSetXorGroup( group, exception, prop, xor ) {
1717
- if (!group)
1718
- return true;
1719
- const siblingprop = xor[group];
1720
- if (!siblingprop) {
1721
- xor[group] = prop;
1722
- return true;
1723
- }
1724
- if (siblingprop === exception)
1797
+ /**
1798
+ * @param {string[]} groups
1799
+ * @param {string} exception
1800
+ * @param {string} prop
1801
+ * @param {object} xor
1802
+ * @return {boolean}
1803
+ */
1804
+ function checkAndSetXorGroup( groups, exception, prop, xor ) {
1805
+ if (!groups || groups.length === 0)
1725
1806
  return true;
1726
- error( 'syntax-unexpected-property', location(true), { '#': 'sibling', prop, siblingprop } );
1727
- return false;
1807
+ let silent = false;
1808
+ return groups.every((group) => {
1809
+ const siblingprop = xor[group];
1810
+ if (!siblingprop) {
1811
+ xor[group] = prop;
1812
+ return true;
1813
+ }
1814
+ if (siblingprop === exception)
1815
+ return true;
1816
+ if (!silent) {
1817
+ error( 'syntax-unexpected-property', location(true), { '#': 'sibling', prop, siblingprop } );
1818
+ silent = true;
1819
+ }
1820
+ return false;
1821
+ });
1728
1822
  }
1729
1823
 
1730
1824
  function implicitName( ref ) {
@@ -1824,6 +1918,7 @@ function popLocation( obj ) {
1824
1918
  function resetHeapModuleVars() {
1825
1919
  vocabInDefinitions = null;
1826
1920
  dollarLocations = [];
1921
+ userOptions = null;
1827
1922
  message = () => undefined;
1828
1923
  error = () => undefined;
1829
1924
  warning = () => undefined;
@@ -1848,6 +1943,7 @@ function toXsn( csn, filename, options, messageFunctions ) {
1848
1943
  arrayLevelCount = 0;
1849
1944
  inExtensions = null;
1850
1945
  vocabInDefinitions = null;
1946
+ userOptions = options;
1851
1947
 
1852
1948
  const xsn = { $frontend: 'json' };
1853
1949