@sap/cds-compiler 2.11.4 → 2.12.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 (80) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/bin/cds_update_identifiers.js +7 -7
  3. package/bin/cdsc.js +9 -10
  4. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  5. package/doc/CHANGELOG_BETA.md +12 -0
  6. package/lib/api/main.js +2 -0
  7. package/lib/api/options.js +2 -2
  8. package/lib/base/message-registry.js +31 -2
  9. package/lib/base/model.js +1 -0
  10. package/lib/base/optionProcessorHelper.js +97 -69
  11. package/lib/checks/.eslintrc.json +2 -0
  12. package/lib/checks/actionsFunctions.js +2 -1
  13. package/lib/checks/foreignKeys.js +4 -4
  14. package/lib/checks/managedInType.js +4 -4
  15. package/lib/checks/queryNoDbArtifacts.js +1 -3
  16. package/lib/checks/sql-snippets.js +93 -0
  17. package/lib/checks/validator.js +8 -0
  18. package/lib/compiler/assert-consistency.js +5 -3
  19. package/lib/compiler/base.js +0 -1
  20. package/lib/compiler/checks.js +32 -9
  21. package/lib/compiler/definer.js +25 -4
  22. package/lib/compiler/index.js +1 -1
  23. package/lib/compiler/propagator.js +3 -2
  24. package/lib/compiler/resolver.js +97 -6
  25. package/lib/compiler/shared.js +12 -1
  26. package/lib/compiler/utils.js +7 -0
  27. package/lib/edm/annotations/genericTranslation.js +34 -17
  28. package/lib/edm/annotations/preprocessAnnotations.js +1 -1
  29. package/lib/edm/csn2edm.js +1 -1
  30. package/lib/edm/edm.js +8 -8
  31. package/lib/edm/edmPreprocessor.js +30 -23
  32. package/lib/edm/edmUtils.js +11 -12
  33. package/lib/gen/Dictionary.json +82 -40
  34. package/lib/gen/language.checksum +1 -1
  35. package/lib/gen/language.interp +3 -1
  36. package/lib/gen/language.tokens +15 -14
  37. package/lib/gen/languageLexer.interp +9 -1
  38. package/lib/gen/languageLexer.js +830 -779
  39. package/lib/gen/languageLexer.tokens +7 -6
  40. package/lib/gen/languageParser.js +2401 -2282
  41. package/lib/json/from-csn.js +47 -16
  42. package/lib/json/to-csn.js +17 -5
  43. package/lib/language/antlrParser.js +3 -3
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/genericAntlrParser.js +68 -51
  46. package/lib/language/language.g4 +128 -74
  47. package/lib/language/multiLineStringParser.js +536 -0
  48. package/lib/main.d.ts +5 -3
  49. package/lib/main.js +3 -2
  50. package/lib/model/csnRefs.js +116 -68
  51. package/lib/model/csnUtils.js +40 -48
  52. package/lib/model/enrichCsn.js +30 -14
  53. package/lib/optionProcessor.js +3 -3
  54. package/lib/render/DuplicateChecker.js +1 -1
  55. package/lib/render/manageConstraints.js +1 -1
  56. package/lib/render/toCdl.js +193 -79
  57. package/lib/render/toHdbcds.js +179 -95
  58. package/lib/render/toRename.js +7 -10
  59. package/lib/render/toSql.js +57 -40
  60. package/lib/render/utils/common.js +24 -5
  61. package/lib/render/utils/sql.js +6 -4
  62. package/lib/transform/braceExpression.js +4 -2
  63. package/lib/transform/db/associations.js +389 -0
  64. package/lib/transform/db/cdsPersistence.js +150 -0
  65. package/lib/transform/db/constraints.js +6 -4
  66. package/lib/transform/db/draft.js +3 -2
  67. package/lib/transform/db/expansion.js +4 -5
  68. package/lib/transform/db/flattening.js +5 -6
  69. package/lib/transform/db/temporal.js +236 -0
  70. package/lib/transform/db/transformExists.js +36 -23
  71. package/lib/transform/forHanaNew.js +35 -626
  72. package/lib/transform/forOdataNew.js +5 -4
  73. package/lib/transform/localized.js +3 -14
  74. package/lib/transform/odata/generateForeignKeyElements.js +2 -2
  75. package/lib/transform/transformUtilsNew.js +13 -13
  76. package/lib/transform/translateAssocsToJoins.js +8 -8
  77. package/lib/transform/universalCsnEnricher.js +217 -47
  78. package/lib/utils/file.js +2 -1
  79. package/lib/utils/timetrace.js +8 -2
  80. package/package.json +1 -1
@@ -110,7 +110,8 @@ function checkActionOrFunction(art, artName, prop, path) {
110
110
  * @param {CSN.Path} currPath The current path
111
111
  */
112
112
  function checkUserDefinedType(type, typeName, currPath) {
113
- if (!isBuiltinType(type) && type.kind && type.kind !== 'type') {
113
+ // TODO: isBuiltinType does not resolve any type-chains.
114
+ if (!isBuiltinType(type.type) && type.kind && type.kind !== 'type') {
114
115
  const serviceOfType = this.csnUtils.getServiceName(typeName);
115
116
  if (serviceName && serviceName !== serviceOfType) {
116
117
  // if (!(isMultiSchema && serviceOfType)) {
@@ -18,12 +18,12 @@ function validateForeignKeys(member) {
18
18
 
19
19
  // Declared as arrow-function to keep scope the same (this value)
20
20
  const handleAssociation = (mem) => {
21
- for (let i = 0; i < mem.keys.length; i++) {
22
- if (mem.keys[i].ref) {
23
- if (!mem.keys[i]._art)
21
+ for (const key of mem.keys) {
22
+ if (key.ref) {
23
+ if (!key._art)
24
24
  continue;
25
25
  // eslint-disable-next-line no-use-before-define
26
- checkForItems(mem.keys[i]._art);
26
+ checkForItems(key._art);
27
27
  }
28
28
  }
29
29
  };
@@ -13,11 +13,11 @@
13
13
  function checkUsedTypesForAnonymousAspectComposition(member) {
14
14
  // Declared as arrow-function to keep scope the same (this value)
15
15
  const handleAssociation = (mem, fn) => {
16
- for (let i = 0; i < mem.keys.length; i++) {
17
- if (mem.keys[i].ref) {
18
- if (!mem.keys[i]._art)
16
+ for (const key of mem.keys) {
17
+ if (key.ref) {
18
+ if (!key._art)
19
19
  continue;
20
- fn(mem.keys[i]._art);
20
+ fn(key._art);
21
21
  }
22
22
  }
23
23
  };
@@ -124,10 +124,8 @@ function checkQueryForNoDBArtifacts(query) {
124
124
  for (const prop of generalQueryProperties) {
125
125
  const queryPart = (query.SELECT || query.SET)[prop];
126
126
  if (Array.isArray(queryPart)) {
127
- for (let i = 0; i < queryPart.length; i++) {
128
- const part = queryPart[i];
127
+ for (const part of queryPart)
129
128
  checkRef(part, prop === 'columns');
130
- }
131
129
  }
132
130
  else if (typeof queryPart === 'object') {
133
131
  checkRef(queryPart, prop === 'columns');
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const { isBetaEnabled } = require('../base/model');
4
+
5
+ // Only to be used with validator.js - a correct this value needs to be provided!
6
+
7
+ /**
8
+ * Check that @sql.prepend annotation is not used on any elements and @sql.append is not used on elements in views.
9
+ *
10
+ * @param {CSN.Element} member
11
+ * @param {string} memberName
12
+ * @param {string} prop
13
+ * @param {CSN.Path} path
14
+ * @returns {void}
15
+ */
16
+ function checkSqlAnnotationOnElement(member, memberName, prop, path) {
17
+ if (isBetaEnabled(this.options, 'sqlSnippets')) {
18
+ if (member['@sql.replace'])
19
+ this.error(null, path, { anno: 'sql.replace' }, `Annotation $(ANNO) is reserved and must not be used`);
20
+ if (member['@sql.prepend'])
21
+ this.error('anno-invalid-sql-element', path, { anno: 'sql.prepend' }, `Annotation $(ANNO) can't be used on elements` );
22
+
23
+ if (member['@sql.append']) {
24
+ if (this.artifact.query)
25
+ this.error('anno-invalid-sql-view-element', path, { anno: 'sql.append' }, `Annotation $(ANNO) can't be used on elements in views` );
26
+ else if (this.csnUtils.isStructured(member))
27
+ this.error('anno-invalid-sql-struct', path, { anno: 'sql.append' }, `Annotation $(ANNO) can't be used on structured elements` );
28
+ else
29
+ checkValidAnnoValue(member, '@sql.append', path, this.error, this.options);
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @param {object} carrier element which has the annotation
36
+ * @param {string} annotation
37
+ * @param {CSN.Path} path
38
+ * @param {Function} error
39
+ * @param {CSN.Options} options
40
+ */
41
+ function checkValidAnnoValue(carrier, annotation, path, error, options) {
42
+ if (carrier[annotation] !== undefined && carrier[annotation] !== null) {
43
+ if (typeof carrier[annotation] !== 'string')
44
+ error(null, path, { anno: annotation.slice(1), type: typeof carrier[annotation] }, `Annotation $(ANNO) must be a string, found $(TYPE)` );
45
+ else if (options.transformation === 'sql') // HDI and HDBCDS do their own checks
46
+ guardAgainstInjection(annotation, carrier[annotation], path, error);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check that @sql.prepend is not used on views - only supported for entities (tables)
52
+ *
53
+ * @param {CSN.Artifact} artifact
54
+ * @param {string} artifactName
55
+ */
56
+ function checkSqlAnnotationOnArtifact(artifact, artifactName) {
57
+ if (isBetaEnabled(this.options, 'sqlSnippets')) {
58
+ if (artifact['@sql.prepend']) {
59
+ if (artifact.query)
60
+ this.error('anno-invalid-sql-view', [ 'definitions', artifactName ], { name: '@sql.prepend' }, `Annotation $(NAME) can't be used on views` );
61
+ else
62
+ checkValidAnnoValue(artifact, '@sql.prepend', [ 'definitions', artifactName ], this.error, this.options);
63
+ }
64
+
65
+ if (artifact['@sql.replace'])
66
+ this.error(null, [ 'definitions', artifactName ], { anno: 'sql.replace' }, `Annotation $(ANNO) is reserved and must not be used`);
67
+
68
+ checkValidAnnoValue(artifact, '@sql.append', [ 'definitions', artifactName ], this.error, this.options);
69
+ }
70
+ }
71
+
72
+ // Anything that could terminate the "old" statement and start a new one basically.
73
+ const invalidInSnippet = [ ';', '--', '/*', '*/' ];
74
+
75
+ /**
76
+ * Check that the common characters used to terminate the current statement and start a fresh one are not used.
77
+ *
78
+ * @param {string} annoName
79
+ * @param {string} annoValue
80
+ * @param {CSN.Path} path
81
+ * @param {Function} error
82
+ */
83
+ function guardAgainstInjection(annoName, annoValue, path, error) {
84
+ for (const invalid of invalidInSnippet) {
85
+ if (annoValue.indexOf(invalid) !== -1) // These should probably not be configurable, right?
86
+ error(null, path, { name: annoName, prop: invalid }, 'Annotation $(NAME) must not contain $(PROP)');
87
+ }
88
+ }
89
+
90
+ module.exports = {
91
+ checkSqlAnnotationOnArtifact,
92
+ checkSqlAnnotationOnElement,
93
+ };
@@ -35,6 +35,10 @@ const checkExplicitlyNullableKeys = require('./nullableKeys');
35
35
  const nonexpandableStructuredInExpression = require('./nonexpandableStructured');
36
36
  const unknownMagic = require('./unknownMagic');
37
37
  const managedWithoutKeys = require('./managedWithoutKeys');
38
+ const {
39
+ checkSqlAnnotationOnArtifact,
40
+ checkSqlAnnotationOnElement,
41
+ } = require('./sql-snippets');
38
42
 
39
43
  const forHanaMemberValidators
40
44
  = [
@@ -45,6 +49,8 @@ const forHanaMemberValidators
45
49
  checkExplicitlyNullableKeys,
46
50
  managedWithoutKeys,
47
51
  warnAboutDefaultOnAssociationForHanaCds,
52
+ // sql.prepend/append
53
+ checkSqlAnnotationOnElement,
48
54
  ];
49
55
 
50
56
  const forHanaArtifactValidators
@@ -53,6 +59,8 @@ const forHanaArtifactValidators
53
59
  validateCdsPersistenceAnnotation,
54
60
  // virtual items are not persisted on the db
55
61
  checkForEmptyOrOnlyVirtual,
62
+ // sql.prepend/append
63
+ checkSqlAnnotationOnArtifact,
56
64
  ];
57
65
 
58
66
  const forHanaCsnValidators = [ nonexpandableStructuredInExpression, unknownMagic ];
@@ -424,9 +424,12 @@ function assertConsistency( model, stage ) {
424
424
  val: {
425
425
  test: isVal, // the following for array/struct value
426
426
  requires: [ 'location' ],
427
- optional: [ 'literal', 'val', 'sym', 'struct', 'variant', 'path', 'name', '$duplicate' ],
427
+ optional: [
428
+ 'literal', 'val', 'sym', 'struct', 'variant', 'path', 'name', '$duplicate', 'upTo',
429
+ ],
428
430
  // TODO: restrict path to #simplePath
429
431
  },
432
+ upTo: { test: TODO },
430
433
  struct: { inherits: 'val', test: isDictionary( definition ) }, // def because double @
431
434
  args: {
432
435
  inherits: 'value',
@@ -792,8 +795,7 @@ function assertConsistency( model, stage ) {
792
795
  }
793
796
 
794
797
  function at( nodes, prop, name ) {
795
- // eslint-disable-next-line no-nested-ternary
796
- const n = name ? (typeof name === 'number' ? ` for index ${ name }` : ` for "${ name }"`) : '';
798
+ const n = name && (typeof name === 'number' ? ` for index ${ name }` : ` for "${ name }"`) || '';
797
799
  const loc = nodes.find( o => o && typeof o === 'object' && (o.location || o.start) );
798
800
  const f = (prop) ? `${ n } in property '${ prop }'` : n;
799
801
  const l = locationString( loc && loc.location || loc || model.location );
@@ -1,6 +1,5 @@
1
1
  // Base Definitions for the Core Compiler
2
2
 
3
-
4
3
  'use strict';
5
4
 
6
5
  const dictKinds = {
@@ -87,6 +87,17 @@ function check( model ) { // = XSN
87
87
  'Keyword “localized” may only be used in combination with string types');
88
88
  }
89
89
  }
90
+ // "key" keyword at localized element in SELECT list.
91
+ // TODO: This check should be moved to localized.js
92
+ if (elem.key && elem.key.val && elem._main && elem._main.query) {
93
+ // original element is localized but not key, as that would have
94
+ // already resulted in a warning
95
+ if (elem._origin && elem._origin.localized && elem._origin.localized.val &&
96
+ ( !elem._origin.key || !elem._origin.key.val)) {
97
+ warning('localized-key', [ elem.key.location, elem ], { keyword: 'localized' },
98
+ 'Keyword $(KEYWORD) is ignored for primary keys');
99
+ }
100
+ }
90
101
  }
91
102
 
92
103
  function checkQuery( query ) {
@@ -334,11 +345,17 @@ function check( model ) { // = XSN
334
345
  // Max cardinalities must be a positive number or '*'
335
346
  for (const prop of [ 'sourceMax', 'targetMax' ]) {
336
347
  if (elem.cardinality[prop]) {
337
- if (!(elem.cardinality[prop].literal === 'number' && elem.cardinality[prop].val > 0 ||
338
- elem.cardinality[prop].literal === 'string' && elem.cardinality[prop].val === '*')) {
339
- error(null, [ elem.cardinality[prop].location, elem ],
340
- { code: elem.cardinality[prop].val },
341
- 'Illegal value $(CODE) for max cardinality (must be a positive number or "*")');
348
+ const { literal, val, location } = elem.cardinality[prop];
349
+ if (!(literal === 'number' && val > 0 ||
350
+ literal === 'string' && val === '*')) {
351
+ error('invalid-cardinality', [ location, elem ], { '#': prop, code: val }, {
352
+ // eslint-disable-next-line max-len
353
+ std: 'Value $(CODE) is invalid for maximum cardinality, expecting a positive number or ‘*’',
354
+ // eslint-disable-next-line max-len
355
+ sourceMax: 'Value $(CODE) is invalid for maximum source cardinality, expecting a positive number or ‘*’',
356
+ // eslint-disable-next-line max-len
357
+ targetMax: 'Value $(CODE) is invalid for maximum target cardinality, expecting a positive number or ‘*’',
358
+ });
342
359
  }
343
360
  }
344
361
  }
@@ -348,10 +365,16 @@ function check( model ) { // = XSN
348
365
  // from-csn.json (expected non-negative number)
349
366
  for (const prop of [ 'sourceMin', 'targetMin' ]) {
350
367
  if (elem.cardinality[prop]) {
351
- if (!(elem.cardinality[prop].literal === 'number' && elem.cardinality[prop].val >= 0)) {
352
- error(null, [ elem.cardinality[prop].location, elem ],
353
- { code: elem.cardinality[prop].val },
354
- 'Illegal value $(CODE) for min cardinality (must be a non-negative number)');
368
+ const { literal, val, location } = elem.cardinality[prop];
369
+ if (!(literal === 'number' && val >= 0)) {
370
+ error('invalid-cardinality', [ location, elem ], { '#': prop, code: val }, {
371
+ // eslint-disable-next-line max-len
372
+ std: 'Value $(CODE) is invalid for minimum cardinality, expecting a non-negative number',
373
+ // eslint-disable-next-line max-len
374
+ targetMin: 'Value $(CODE) is invalid for minimum target cardinality, expecting a non-negative number',
375
+ // eslint-disable-next-line max-len
376
+ sourceMin: 'Value $(CODE) is invalid for minimum source cardinality, expecting a non-negative number',
377
+ });
355
378
  }
356
379
  }
357
380
  }
@@ -370,7 +370,7 @@ function define( model ) {
370
370
  // TODO: check name: no "."
371
371
  if (path[0].id === 'localized' || path[0].id.startsWith( 'localized.' )) {
372
372
  decl.$inferred = 'LOCALIZED-IGNORED';
373
- warning( 'using-localized-view', [ path.location, decl ], {},
373
+ warning( 'using-localized-view', [ decl.location, decl ], {},
374
374
  'Localization views can\'t be referred to - ignored USING' );
375
375
  // actually not ignored anymore
376
376
  }
@@ -1152,7 +1152,8 @@ function define( model ) {
1152
1152
  break; // only direct projection for auto-exposed
1153
1153
  }
1154
1154
  let ancestors = art && (!autoexposed && art._ancestors || []);
1155
- for (const a of chain.reverse()) {
1155
+ chain.reverse();
1156
+ for (const a of chain) {
1156
1157
  ancestors = (ancestors ? [ ...ancestors, art ] : []);
1157
1158
  setProp( a, '_ancestors', ancestors );
1158
1159
  art = a;
@@ -1274,6 +1275,9 @@ function define( model ) {
1274
1275
  }
1275
1276
  }
1276
1277
 
1278
+ /**
1279
+ * @returns {boolean|0} `true`, if allowed, `false` if forbidden, `0` if circular containment.
1280
+ */
1277
1281
  function allowAspectComposition( target, elem, keys, entityName ) {
1278
1282
  if (!target.elements || Object.values( target.elements ).some( e => e.$duplicates ))
1279
1283
  return false; // no elements or with redefinitions
@@ -1520,8 +1524,15 @@ function define( model ) {
1520
1524
  forEachMemberRecursivelyWithQuery(art, checkArtifact);
1521
1525
  }
1522
1526
 
1523
- // Function for parse.cdl
1524
- /** @param {XSN.Artifact} artifact */
1527
+ /**
1528
+ * Function for parse.cdl.
1529
+ * This parse.cdl function resolves types inside the artifact ("resolveTypeUnchecked").
1530
+ *
1531
+ * @todo This function needs to be properly reworked because at the moment we simply
1532
+ * added checks as issues arose (e.g. for "artifact.value").
1533
+ *
1534
+ * @param {XSN.Artifact} artifact
1535
+ * */
1525
1536
  function checkArtifact( artifact ) {
1526
1537
  // columns are initialized (and made to elements) in the resolver - do init here
1527
1538
  for (const col of artifact.columns || []) {
@@ -1532,11 +1543,21 @@ function define( model ) {
1532
1543
  recursivelyResolveExpressionCastTypes(col.value, artifact);
1533
1544
  }
1534
1545
 
1546
+ // Possible inside expand/inline
1547
+ if (artifact.value)
1548
+ recursivelyResolveExpressionCastTypes(artifact.value, artifact);
1549
+
1535
1550
  resolveTypeUnchecked(artifact, artifact);
1536
1551
 
1537
1552
  if (artifact.items)
1538
1553
  resolveTypeUnchecked(artifact.items, artifact);
1539
1554
 
1555
+ if (Array.isArray(artifact.expand))
1556
+ artifact.expand.forEach(art => checkArtifact(art));
1557
+
1558
+ if (Array.isArray(artifact.inline))
1559
+ artifact.inline.forEach(art => checkArtifact(art));
1560
+
1540
1561
  for (const include of (artifact.includes || []))
1541
1562
  resolveUncheckedPath(include, 'include', artifact);
1542
1563
 
@@ -64,7 +64,7 @@ class ArgumentError extends Error {
64
64
  * @param {object} options Compile options
65
65
  * @param {object} messageFunctions If not provided, parse errors will not lead to an exception
66
66
  */
67
- function parseX( source, filename, options = {}, messageFunctions ) {
67
+ function parseX( source, filename, options = {}, messageFunctions = null ) {
68
68
  if (!messageFunctions)
69
69
  messageFunctions = createMessageFunctions( options, 'parse' );
70
70
  const ext = path.extname( filename ).toLowerCase();
@@ -19,6 +19,8 @@ function propagate( model ) {
19
19
  '@cds.persistence.calcview': never,
20
20
  '@cds.persistence.udf': never,
21
21
  '@cds.persistence.skip': notWithPersistenceTable,
22
+ '@sql.prepend': never,
23
+ '@sql.append': never,
22
24
  '@Analytics.hidden': never,
23
25
  '@Analytics.visible': never,
24
26
  '@cds.autoexpose': onlyViaArtifact,
@@ -64,8 +66,7 @@ function propagate( model ) {
64
66
  if (!art)
65
67
  return;
66
68
  if (!checkAndSetStatus( art )) {
67
- if ( art.status !== 'propagated')
68
- runMembers( art );
69
+ runMembers( art );
69
70
  return;
70
71
  }
71
72
  // console.log('RUN:', art.name, art.elements ? Object.keys(art.elements) : 0)
@@ -54,6 +54,7 @@ const {
54
54
  setLink,
55
55
  annotationVal,
56
56
  augmentPath,
57
+ pathName,
57
58
  splitIntoPath,
58
59
  linkToOrigin,
59
60
  setMemberParent,
@@ -1108,7 +1109,6 @@ function resolve( model ) {
1108
1109
  // or use userQuery( query ) in the following, too?
1109
1110
  setMemberParent( col, `.${ q.$inlines.length }`, query );
1110
1111
  initFromColumns( query, col.inline, col );
1111
- continue;
1112
1112
  }
1113
1113
  else if (!col.$replacement) {
1114
1114
  const id = ensureColumnName( col, query );
@@ -1602,9 +1602,17 @@ function resolve( model ) {
1602
1602
  }
1603
1603
  }
1604
1604
 
1605
- function annotateMembers( art, extensions = [], prop, name, parent, kind ) {
1605
+ /**
1606
+ * @param {XSN.Artifact} art
1607
+ * @param {XSN.Extension[]} [extensions]
1608
+ * @param {string} [prop]
1609
+ * @param {string} [name]
1610
+ * @param {object} [parent]
1611
+ * @param {string} [kind]
1612
+ */
1613
+ function annotateMembers( art, extensions, prop, name, parent, kind ) {
1606
1614
  const showMsg = !art && parent && parent.kind !== 'annotate';
1607
- if (!art && extensions.length) {
1615
+ if (!art && extensions && extensions.length) {
1608
1616
  if (Array.isArray( parent ))
1609
1617
  return;
1610
1618
  const parentExt = extensionFor(parent);
@@ -1620,7 +1628,7 @@ function resolve( model ) {
1620
1628
  }
1621
1629
  }
1622
1630
 
1623
- for (const ext of extensions) {
1631
+ for (const ext of extensions || []) {
1624
1632
  if ('_artifact' in ext.name) // already applied
1625
1633
  continue;
1626
1634
  setProp( ext.name, '_artifact', art );
@@ -1849,7 +1857,7 @@ function resolve( model ) {
1849
1857
  [ mergeSource.name.location, art ], { code: '...' } );
1850
1858
  return;
1851
1859
  }
1852
- mergeTarget.val.splice(pos, 1, ...mergeSource.val);
1860
+ mergeTarget.val = mergeArrayValues( mergeSource.val, mergeTarget.val );
1853
1861
  }
1854
1862
  }
1855
1863
  });
@@ -1869,7 +1877,7 @@ function resolve( model ) {
1869
1877
  [ mergeSource.name.location, art ], { code: '...' } );
1870
1878
  return mergeTarget;
1871
1879
  }
1872
- mergeTarget.val.splice(pos, 1, ...mergeSource.val);
1880
+ mergeTarget.val = mergeArrayValues( mergeSource.val, mergeTarget.val );
1873
1881
  layer = layers.layer( mergeSource._block );
1874
1882
  delete layerAnnos[(layer) ? layer.realname : ''];
1875
1883
  pos = findEllipsis( mergeTarget );
@@ -1880,6 +1888,89 @@ function resolve( model ) {
1880
1888
  return mergeTarget;
1881
1889
  }
1882
1890
 
1891
+ function mergeArrayValues( previousValue, arraySpec ) {
1892
+ let prevPos = 0;
1893
+ const result = [];
1894
+ for (const item of arraySpec) {
1895
+ const ell = item && item.literal === 'token' && item.val === '...';
1896
+ if (!ell) {
1897
+ result.push( item );
1898
+ }
1899
+ else {
1900
+ let upToSpec = item.upTo && checkUpToSpec( item.upTo, true );
1901
+ while (prevPos < previousValue.length) {
1902
+ const prevItem = previousValue[prevPos++];
1903
+ result.push( prevItem );
1904
+ if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) {
1905
+ upToSpec = false;
1906
+ break;
1907
+ }
1908
+ }
1909
+ if (upToSpec) { // non-matched UP TO
1910
+ warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' },
1911
+ 'The $(CODE) value does not match any item in the base annotation $(ANNO)' );
1912
+ }
1913
+ }
1914
+ }
1915
+ return result;
1916
+ }
1917
+
1918
+ function checkUpToSpec( upToSpec, trueIfFullUpTo ) {
1919
+ const { literal } = upToSpec;
1920
+ if (trueIfFullUpTo !== true) { // inside struct of UP TO
1921
+ if (![ 'struct', 'array' ].includes( literal ))
1922
+ return true;
1923
+ }
1924
+ else if (literal === 'struct') {
1925
+ return Object.values( upToSpec.struct ).every( checkUpToSpec );
1926
+ }
1927
+ else if (![ 'array', 'boolean', 'null' ].includes( literal )) {
1928
+ return true;
1929
+ }
1930
+ error( null, [ upToSpec.location, art ],
1931
+ { anno: annoName, code: '... up to', '#': literal },
1932
+ {
1933
+ std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)',
1934
+ array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)',
1935
+ // eslint-disable-next-line max-len
1936
+ struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)',
1937
+ boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)',
1938
+ null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)',
1939
+ } );
1940
+ return false;
1941
+ }
1942
+
1943
+ function equalUpTo( previousItem, upToSpec ) {
1944
+ if (!previousItem)
1945
+ return false;
1946
+ if ('val' in upToSpec) {
1947
+ if (previousItem.val === upToSpec.val) // enum, struct and ref have no val
1948
+ return true;
1949
+ const typeUpTo = typeof upToSpec.val;
1950
+ const typePrev = typeof previousItem.val;
1951
+ if (typeUpTo === 'number')
1952
+ return typePrev === 'string' && previousItem.val === upToSpec.val.toString();
1953
+ if (typePrev === 'number')
1954
+ return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString();
1955
+ }
1956
+ else if (upToSpec.path) {
1957
+ return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec );
1958
+ }
1959
+ else if (upToSpec.sym) {
1960
+ return previousItem.sym && previousItem.sym.id === upToSpec.sym.id;
1961
+ }
1962
+ else if (upToSpec.struct && previousItem.struct) {
1963
+ return Object.entries( upToSpec.struct )
1964
+ .every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) );
1965
+ }
1966
+ return false;
1967
+ }
1968
+
1969
+ function normalizeRef( node ) { // see to-csn.js
1970
+ const ref = pathName( node.path );
1971
+ return node.variant ? `${ ref }#${ node.variant.id }` : ref;
1972
+ }
1973
+
1883
1974
  function removeEllipsis(a, pos = findEllipsis( a )) {
1884
1975
  let count = 0;
1885
1976
  while (a.literal === 'array' && pos > -1) {
@@ -26,7 +26,6 @@ function artifactsEnv( art ) {
26
26
  */
27
27
  // TODO: yes, this function will be renamed
28
28
  function fns( model ) {
29
- /** @type {CSN.Options} */
30
29
  const { options } = model;
31
30
  const {
32
31
  info, warning, error, message,
@@ -173,16 +172,25 @@ function fns( model ) {
173
172
  return !(art.elements && !art.query && !art.type && !art.params);
174
173
  }
175
174
 
175
+ /**
176
+ * @returns {boolean|string}
177
+ */
176
178
  function checkTypeRef( art ) {
177
179
  if (art.kind === 'type' || art.kind === 'element')
178
180
  return false;
179
181
  return ![ 'entity', 'aspect', 'event' ].includes( art.kind ) || 'sloppy';
180
182
  }
181
183
 
184
+ /**
185
+ * @returns {boolean|string}
186
+ */
182
187
  function checkActionParamTypeRef( art ) {
183
188
  return !(art.kind === 'entity' && art._service) && checkTypeRef( art );
184
189
  }
185
190
 
191
+ /**
192
+ * @returns {boolean|string}
193
+ */
186
194
  function checkEventTypeRef( art ) {
187
195
  return art.kind !== 'event' && checkActionParamTypeRef( art );
188
196
  }
@@ -191,6 +199,9 @@ function fns( model ) {
191
199
  return art.kind !== 'entity';
192
200
  }
193
201
 
202
+ /**
203
+ * @returns {boolean|string}
204
+ */
194
205
  function checkTargetRef( art ) {
195
206
  if (art.kind === 'entity' || art.kind === 'aspect')
196
207
  return false;
@@ -30,6 +30,13 @@ function annotationIsFalse( anno ) { // falsy, but not null (u
30
30
  return anno && (anno.val === false || anno.val === 0 || anno.val === '');
31
31
  }
32
32
 
33
+ /**
34
+ * @param {XSN.Artifact} art
35
+ * @param {string} anno
36
+ * @param {XSN.Location} [location]
37
+ * @param {*} [val]
38
+ * @param {string} [literal]
39
+ */
33
40
  function annotateWith( art, anno, location = art.location, val = true, literal = 'boolean' ) {
34
41
  if (art[anno]) // do not overwrite user-defined including null
35
42
  return;