@sap/cds-compiler 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/bin/.eslintrc.json +2 -1
  3. package/bin/cdsc.js +19 -0
  4. package/doc/API.md +11 -0
  5. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  6. package/doc/CHANGELOG_BETA.md +24 -2
  7. package/doc/CHANGELOG_DEPRECATED.md +21 -1
  8. package/lib/api/main.js +7 -7
  9. package/lib/api/options.js +2 -3
  10. package/lib/base/message-registry.js +17 -5
  11. package/lib/base/messages.js +18 -39
  12. package/lib/base/model.js +2 -0
  13. package/lib/checks/actionsFunctions.js +8 -7
  14. package/lib/checks/selectItems.js +96 -14
  15. package/lib/checks/types.js +5 -8
  16. package/lib/checks/validator.js +1 -2
  17. package/lib/compiler/assert-consistency.js +64 -12
  18. package/lib/compiler/base.js +6 -4
  19. package/lib/compiler/builtins.js +58 -8
  20. package/lib/compiler/checks.js +1 -1
  21. package/lib/compiler/define.js +25 -22
  22. package/lib/compiler/extend.js +16 -10
  23. package/lib/compiler/finalize-parse-cdl.js +5 -9
  24. package/lib/compiler/index.js +2 -0
  25. package/lib/compiler/populate.js +34 -31
  26. package/lib/compiler/propagator.js +11 -6
  27. package/lib/compiler/resolve.js +14 -15
  28. package/lib/compiler/shared.js +53 -26
  29. package/lib/compiler/tweak-assocs.js +5 -11
  30. package/lib/compiler/utils.js +13 -4
  31. package/lib/edm/annotations/preprocessAnnotations.js +8 -4
  32. package/lib/edm/csn2edm.js +3 -3
  33. package/lib/edm/edm.js +9 -1
  34. package/lib/edm/edmAnnoPreprocessor.js +349 -0
  35. package/lib/edm/edmInboundChecks.js +85 -0
  36. package/lib/edm/edmPreprocessor.js +295 -638
  37. package/lib/edm/edmUtils.js +85 -5
  38. package/lib/gen/Dictionary.json +29 -9
  39. package/lib/gen/language.checksum +1 -1
  40. package/lib/gen/language.interp +1 -2
  41. package/lib/gen/languageLexer.js +3 -0
  42. package/lib/gen/languageParser.js +4344 -4530
  43. package/lib/inspect/.eslintrc.json +4 -0
  44. package/lib/inspect/index.js +14 -0
  45. package/lib/inspect/inspectModelStatistics.js +81 -0
  46. package/lib/inspect/inspectPropagation.js +189 -0
  47. package/lib/inspect/inspectUtils.js +44 -0
  48. package/lib/json/from-csn.js +3 -2
  49. package/lib/json/to-csn.js +8 -6
  50. package/lib/language/genericAntlrParser.js +121 -63
  51. package/lib/language/language.g4 +19 -57
  52. package/lib/main.d.ts +1 -0
  53. package/lib/model/api.js +1 -1
  54. package/lib/model/csnRefs.js +55 -29
  55. package/lib/model/csnUtils.js +11 -7
  56. package/lib/model/revealInternalProperties.js +2 -3
  57. package/lib/modelCompare/compare.js +3 -0
  58. package/lib/optionProcessor.js +27 -0
  59. package/lib/render/toCdl.js +57 -32
  60. package/lib/render/toSql.js +24 -8
  61. package/lib/render/utils/common.js +3 -4
  62. package/lib/transform/db/associations.js +43 -35
  63. package/lib/transform/db/cdsPersistence.js +0 -1
  64. package/lib/transform/db/flattening.js +3 -4
  65. package/lib/transform/db/transformExists.js +7 -5
  66. package/lib/transform/draft/db.js +1 -1
  67. package/lib/transform/forHanaNew.js +11 -2
  68. package/lib/transform/forOdataNew.js +1 -1
  69. package/lib/transform/odata/typesExposure.js +14 -5
  70. package/lib/utils/moduleResolve.js +0 -1
  71. package/package.json +2 -2
  72. package/lib/checks/unknownMagic.js +0 -41
@@ -0,0 +1,4 @@
1
+ {
2
+ "root": true,
3
+ "extends": "../../.eslintrc-ydkjsi.json"
4
+ }
@@ -0,0 +1,14 @@
1
+ // cds-compiler Inspect Module
2
+ // Used by `cdsc inspect` to gather details about the model such as statistics, etc.
3
+
4
+ 'use strict';
5
+
6
+ const { inspectModelStatistics } = require('./inspectModelStatistics');
7
+ const { inspectPropagation } = require('./inspectPropagation');
8
+ const { stringRefToPath } = require('./inspectUtils');
9
+
10
+ module.exports = {
11
+ inspectModelStatistics,
12
+ inspectPropagation,
13
+ stringRefToPath,
14
+ };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const { forEach } = require('../utils/objectUtils');
4
+ const { term } = require('../utils/term');
5
+
6
+ /**
7
+ * Return a string representation of the inspected results.
8
+ *
9
+ * @param {XSN.Model} xsn
10
+ * @param {CSN.Options} options
11
+ * @returns {string}
12
+ */
13
+ function inspectModelStatistics(xsn, options) {
14
+ let result = '';
15
+
16
+ // Default color mode is 'auto'
17
+ const color = term(options.color || 'auto');
18
+
19
+ const defCount = countDefinitionKinds(xsn);
20
+ const sources = {
21
+ cdl: Object.keys(xsn.sources).filter(name => xsn.sources[name].$frontend === 'cdl').length,
22
+ csn: Object.keys(xsn.sources).filter(name => xsn.sources[name].$frontend === 'csn').length,
23
+ };
24
+
25
+ result += `cds-compiler model statistics:
26
+
27
+ ${ color.underline('files') }: ${ Object.keys(xsn.sources).length }
28
+ cdl sources: ${ sources.cdl }
29
+ csn sources: ${ sources.csn }
30
+
31
+ ${ color.underline('definitions') }: ${ defCount.definitions }
32
+ entities: ${ defCount.entity }
33
+ queries: ${ defCount.view }
34
+ aspects: ${ defCount.aspect }
35
+ events: ${ defCount.event }
36
+ types: ${ defCount.type }
37
+ services: ${ defCount.service }
38
+ context: ${ defCount.context }
39
+ actions: ${ defCount.action }
40
+ functions: ${ defCount.function }
41
+ namespaces: ${ defCount.namespace } (explicitly in CDL)
42
+
43
+ ${ color.underline('vocabularies') }: ${ Object.keys(xsn.vocabularies || {}).length }
44
+ `;
45
+ return result;
46
+ }
47
+
48
+ function countDefinitionKinds(xsn) {
49
+ const result = {
50
+ definitions: 0,
51
+ entity: 0,
52
+ action: 0,
53
+ function: 0,
54
+ aspect: 0,
55
+ event: 0,
56
+ type: 0,
57
+ service: 0,
58
+ context: 0,
59
+ namespace: 0,
60
+ // non-kind
61
+ view: 0,
62
+ };
63
+ forEach(xsn.definitions || {}, (name, def) => {
64
+ if (def.builtin)
65
+ return;
66
+ ++result.definitions;
67
+
68
+ if (def.query || def.projection)
69
+ ++result.view;
70
+ else if (result[def.kind] !== undefined)
71
+ ++result[def.kind];
72
+ else
73
+ throw new Error(`Unhandled kind: ${ def.kind } for ${ name }`);
74
+ });
75
+ return result;
76
+ }
77
+
78
+
79
+ module.exports = {
80
+ inspectModelStatistics,
81
+ };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const { createMessageFunctions } = require('../base/messages');
4
+ const { locationString } = require('../base/location');
5
+ const { findArtifact, stringRefToPath } = require('./inspectUtils');
6
+ const { term } = require('../utils/term');
7
+
8
+ /**
9
+ * @param {XSN.Model} xsn
10
+ * @param {CSN.Options} options
11
+ * @param {string} artifactName
12
+ * @returns {string|null}
13
+ */
14
+ function inspectPropagation(xsn, options, artifactName) {
15
+ const { error } = createMessageFunctions(options, 'inspect', xsn);
16
+ const result = [];
17
+
18
+ // Default color mode is 'auto'
19
+ const color = term(options.color || 'auto');
20
+
21
+ const path = stringRefToPath(artifactName);
22
+ if (!path) {
23
+ error(null, null, { name: artifactName },
24
+ 'Artifact $(NAME) is not a valid path; expected format `<def>[:element]`');
25
+ return null;
26
+ }
27
+
28
+ const artifactXsn = findArtifact(xsn, path);
29
+
30
+ if (!artifactXsn) {
31
+ error(null, null, { name: artifactName },
32
+ // eslint-disable-next-line max-len
33
+ 'Artifact $(NAME) not found, only top-level artifacts and their elements are supported for now');
34
+ return null;
35
+ }
36
+ result.push(color.underline('analyzing propagation for artifact:'));
37
+ result.push(` name: ${ artifactXsn.name.id }`);
38
+ result.push(` kind: ${ artifactXsn.kind }`);
39
+
40
+ if (artifactXsn.$inferred)
41
+ result.push(` inferred: ${ artifactXsn.$inferred }`);
42
+
43
+ result.push('');
44
+ result.push(` ${ color.underline('annotation propagation:') }`);
45
+ result.push(..._indent(_inspectAnnotations(artifactXsn)));
46
+
47
+ result.push('');
48
+ result.push(` ${ color.underline('element propagation:') }`);
49
+ result.push(..._indent(_inspectElements(artifactXsn)));
50
+
51
+ return result.join('\n');
52
+ }
53
+
54
+ /**
55
+ @param {string[]} lines
56
+ @param {string} indent
57
+ * @returns {string[]}
58
+ */
59
+ function _indent(lines, indent = ' ') {
60
+ return lines.map(str => `${ indent }${ str }`);
61
+ }
62
+
63
+ /**
64
+ * @param {XSN.Artifact} artifactXsn
65
+ * @returns {string[]}
66
+ * @private
67
+ */
68
+ function _inspectAnnotations(artifactXsn) {
69
+ const result = [];
70
+ const annos = Object.keys(artifactXsn).filter(str => str.startsWith('@')).sort();
71
+
72
+ if (annos.length === 0)
73
+ return [ 'no annotations' ];
74
+
75
+ let maxAnnoLength = 30; // chosen arbitrarily, hopefully average
76
+ for (const anno of annos) {
77
+ const annoXsn = artifactXsn[anno];
78
+ const loc = locationString(annoXsn.name.location);
79
+ let origin;
80
+ switch (annoXsn.$priority) {
81
+ case false:
82
+ if (annoXsn.$inferred)
83
+ origin = 'propagation';
84
+ else
85
+ origin = 'direct';
86
+ break;
87
+
88
+ case 'extend':
89
+ case 'annotate':
90
+ origin = annoXsn.$priority;
91
+ break;
92
+
93
+ case undefined:
94
+ if (annoXsn.$inferred === '$generated') {
95
+ origin = 'generated';
96
+ break;
97
+ }
98
+ // fallthrough
99
+ default:
100
+ throw new Error(`inspect anno: Unhandled Case: ${ annoXsn.$priority }`);
101
+ }
102
+
103
+ maxAnnoLength = Math.max(maxAnnoLength, anno.length);
104
+
105
+ // origin: assume max length 11 of 'propagation'
106
+ // anno: use max length of all annotations till now
107
+ result.push([ origin.padStart(11), anno.padEnd(maxAnnoLength), loc ].join(' | '));
108
+ }
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * @param {XSN.Artifact} artifactXsn
114
+ * @returns {string[]}
115
+ * @private
116
+ */
117
+ function _inspectElements(artifactXsn) {
118
+ if (!artifactXsn.elements)
119
+ return [ 'does not have elements' ];
120
+
121
+ const result = [];
122
+ const elements = Object.keys(artifactXsn.elements);
123
+
124
+ let maxElemLength = 12;
125
+
126
+ const inferredNiceOutput = {
127
+ '*': 'wildcard',
128
+ 'expand-element': 'expanded',
129
+ 'expand-param': 'expanded',
130
+ 'aspect-composition': 'composition',
131
+ };
132
+
133
+ for (const element of elements) {
134
+ const elementXsn = artifactXsn.elements[element];
135
+ const loc = locationString(_origin(elementXsn).name.location);
136
+ let origin;
137
+
138
+ if (elementXsn.$inferred) {
139
+ // Use nice(r) output for known $inferred
140
+ if (inferredNiceOutput[elementXsn.$inferred])
141
+ origin = inferredNiceOutput[elementXsn.$inferred];
142
+ else
143
+ origin = elementXsn.$inferred;
144
+ }
145
+ else if (!isContainedInParentLocation(elementXsn, artifactXsn)) {
146
+ // just a heuristic
147
+ origin = 'extend';
148
+ }
149
+ else {
150
+ origin = 'direct';
151
+ }
152
+
153
+ maxElemLength = Math.max(maxElemLength, element.length);
154
+
155
+ // origin: assume max length 11 of 'composition'
156
+ // element: assume average length of 30, chosen randomly
157
+ result.push([ origin.padStart(11), element.padEnd(maxElemLength), loc ].join(' | '));
158
+ }
159
+ return result;
160
+ }
161
+
162
+ function _origin(elementXsn) {
163
+ while (elementXsn._origin)
164
+ elementXsn = elementXsn._origin;
165
+ return elementXsn;
166
+ }
167
+
168
+ /**
169
+ * Returns true if `art` is contained in `parent` according to its location.
170
+ *
171
+ * @param art
172
+ * @param parent
173
+ * @returns {boolean}
174
+ */
175
+ function isContainedInParentLocation(art, parent) {
176
+ const artLoc = art.location;
177
+ const parentLoc = parent.location;
178
+ if (artLoc.file !== parentLoc.file)
179
+ return false;
180
+ if (artLoc.line < parentLoc.line || artLoc.line > parentLoc.endLine)
181
+ return false;
182
+ // Good enough for now
183
+ // TODO: Check columns
184
+ return true;
185
+ }
186
+
187
+ module.exports = {
188
+ inspectPropagation,
189
+ };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Reference (string) to path (array) that can be used to identify an artifact
5
+ * @param str
6
+ * @returns {*[]|*}
7
+ */
8
+ function stringRefToPath(str) {
9
+ // e.g. `ns.service.E:sub.elem.structured`
10
+ const path = str.split(':');
11
+ if (path.length === 1)
12
+ return path;
13
+ if (path.length > 2)
14
+ return null;
15
+ return [ path[0], ...path[1].split('.') ];
16
+ }
17
+
18
+ /**
19
+ * @param {XSN.Model} xsn
20
+ * @param {string} path
21
+ * @private
22
+ */
23
+ function findArtifact(xsn, path) {
24
+ const segments = [ ...path ];
25
+ const topLevelName = segments[0];
26
+ let art = (xsn.definitions && xsn.definitions[topLevelName]) ||
27
+ (xsn.vocabularies && xsn.vocabularies[topLevelName]);
28
+ if (!art)
29
+ return null;
30
+ segments.shift();
31
+ if (segments.length === 0)
32
+ return art;
33
+ while (segments.length && art) {
34
+ const segment = segments.shift();
35
+ art = (art.items?.elements || art.elements)?.[segment];
36
+ }
37
+ return art || null;
38
+ }
39
+
40
+
41
+ module.exports = {
42
+ stringRefToPath,
43
+ findArtifact,
44
+ };
@@ -103,6 +103,7 @@ const ourpropsRegex = /^[_$]?[a-zA-Z]+[0-9]*$/;
103
103
  const typeProperties = [
104
104
  // do not include CSN v0.1.0 properties here:
105
105
  'target', 'elements', 'enum', 'items',
106
+ 'cardinality', // for association publishing in views
106
107
  'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull',
107
108
  'keys', 'on', // only with 'target'
108
109
  ];
@@ -228,7 +229,7 @@ const schema = compileSchema( {
228
229
  dictionaryOf: definition,
229
230
  defaultKind: 'action',
230
231
  validKinds: [ 'action', 'function' ],
231
- inKind: [ 'entity', 'annotate', 'extend' ],
232
+ inKind: [ 'entity', 'aspect', 'annotate', 'extend' ],
232
233
  },
233
234
  params: {
234
235
  dictionaryOf: definition,
@@ -1287,7 +1288,7 @@ function literal( lit, spec, xsn, csn ) {
1287
1288
  return lit;
1288
1289
  if (typeof lit === 'string' && quotedLiteralPatterns[lit]?.json_type === type) {
1289
1290
  const p = quotedLiteralPatterns[lit];
1290
- if (p && (p.test_fn && !p.test_fn(csn.val) || p.test_re && !p.test_re.test(csn.val)))
1291
+ if (p && p.test_fn && !p.test_fn(csn.val))
1291
1292
  warning( 'syntax-invalid-literal', location(), { '#': p.test_variant } );
1292
1293
  return lit;
1293
1294
  }
@@ -127,9 +127,6 @@ const transformers = {
127
127
  // location is not renamed to $location as the name is well established in
128
128
  // XSN and too many places (also outside the compiler) had to be adapted
129
129
  location, // non-enumerable $location in CSN
130
- $a2j: (e, csn) => { // on artifact level
131
- Object.assign( csn, e );
132
- },
133
130
  $extra: (e, csn) => {
134
131
  Object.assign( csn, e );
135
132
  },
@@ -185,9 +182,11 @@ const propertyOrder = (function orderPositions() {
185
182
  }());
186
183
 
187
184
  // sync with definition in from-csn.js:
185
+ // Note: Order here is also the property order in CSN.
188
186
  const typeProperties = [
189
- 'target', 'elements', 'enum', 'items', // TODO: notNull?
190
- 'type', 'length', 'precision', 'scale', 'srid', 'localized',
187
+ 'target', 'elements', 'enum', 'items',
188
+ 'cardinality', // for association publishing in views
189
+ 'type', 'length', 'precision', 'scale', 'srid', 'localized', // TODO: notNull?
191
190
  'foreignKeys', 'on', // for explicit ON/keys with REDIRECTED
192
191
  ];
193
192
 
@@ -1276,8 +1275,11 @@ function expression( node, dollarExtra ) {
1276
1275
  return { ref: [ node.param.val ], param: true }; // CDL rule for runtimes
1277
1276
  }
1278
1277
  if (node.path) {
1278
+ const ref = node.path.map( pathItem );
1279
+ if (node.path.$prefix)
1280
+ ref.unshift( node.path.$prefix );
1279
1281
  // we would need to consider node.global here if we introduce that
1280
- return extra( { ref: node.path.map( pathItem ) }, dollarExtraNode );
1282
+ return extra( { ref }, dollarExtraNode );
1281
1283
  }
1282
1284
  if (node.literal) {
1283
1285
  if (typeof node.val === node.literal || node.val === null)
@@ -63,10 +63,13 @@ Object.assign(GenericAntlrParser.prototype, {
63
63
  info: function(...args) { return _message( this, 'info', ...args ); },
64
64
  attachLocation,
65
65
  assignAnnotation,
66
+ addAnnotation,
66
67
  checkExtensionDict,
67
- handleExtension,
68
+ handleDuplicateExtension,
68
69
  startLocation,
69
70
  tokenLocation,
71
+ isMultiLineToken,
72
+ fixMultiLineTokenEndLocation,
70
73
  valueWithTokenLocation,
71
74
  previousTokenAtLocation,
72
75
  combinedLocation,
@@ -190,12 +193,12 @@ function setLocalTokenIfBefore( string, tokenName, before, inSameLine ) {
190
193
  }
191
194
 
192
195
  function setLocalTokenForId( tokenNameMap ) {
196
+ const tokenName = tokenNameMap[ this._input.LT(2).text || '' ];
193
197
  const ll1 = this.getCurrentToken();
194
- if (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )) {
195
- const tokenName = tokenNameMap[ this._input.LT(2).text || '' ];
196
- if (tokenName)
197
- ll1.type = this.constructor[tokenName];
198
- }
198
+ if (tokenName &&
199
+ (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )))
200
+ ll1.type = this.constructor[tokenName];
201
+ return !!tokenName;
199
202
  }
200
203
 
201
204
  // // Special function for rule `requiredSemi` before return $ctx
@@ -267,9 +270,6 @@ function prepareGenericKeywords( pathItem, expected = null) {
267
270
  const func = pathItem?.id && specialFunctions[pathItem.id.toUpperCase()];
268
271
  const spec = func && func[argPos] || specialFunctions[''][argPos ? 1 : 0];
269
272
  this.$genericKeywords = spec;
270
- // currently, we only have 'TODO', i.e. a keyword which is alternative to expression
271
- // TODO: If not just at the beginning, we need a stack for $genericKeywords,
272
- // as we can have nested special functions
273
273
  // @ts-ignore
274
274
  const token = this.getCurrentToken() || { text: '' };
275
275
  const text = token.text.toUpperCase();
@@ -308,15 +308,26 @@ function reportErrorForGenericKeyword() {
308
308
  function attachLocation( art ) {
309
309
  if (!art || art.$parens)
310
310
  return art;
311
- if (!art.location)
312
- art.location = this.startLocation();
313
- const { stop } = this._ctx;
314
- art.location.endLine = stop.line;
315
- art.location.endCol = stop.stop - stop.start + stop.column + 2; // after the last char (special for EOF?)
311
+ if (!art.location) {
312
+ art.location = this.tokenLocation(this._ctx.start, this._ctx.stop);
313
+ return art;
314
+ }
315
+
316
+ // The last token (this._ctx.stop) may be a multi-line string literal, in which
317
+ // case we can't rely on `this._ctx.stop.line`.
318
+ if (this.isMultiLineToken(this._ctx.stop)) {
319
+ this.fixMultiLineTokenEndLocation(this._ctx.stop, art.location);
320
+
321
+ } else {
322
+ const { stop } = this._ctx;
323
+ art.location.endLine = stop.line;
324
+ art.location.endCol = stop.stop - stop.start + stop.column + 2; // after the last char (special for EOF?)
325
+ }
326
+
316
327
  return art;
317
328
  }
318
329
 
319
- function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
330
+ function assignAnnotation( art, anno, prefix = '', iHaveVariant = false ) {
320
331
  const { name, $flatten } = anno;
321
332
  const { path } = name;
322
333
  if (path.broken || !path[path.length - 1].id)
@@ -344,19 +355,9 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
344
355
  }
345
356
  else {
346
357
  name.absolute = absolute;
347
- const prop = '@' + absolute;
348
- const old = art[prop];
349
- if (old && old.$inferred)
350
- art[prop] = anno;
351
- else
352
- dictAddArray( art, prop, anno, (n, location, a) => {
353
- this.error( 'syntax-duplicate-anno', [ location ], { anno: n },
354
- 'Duplicate assignment with $(ANNO)' );
355
- a.$errorReported = 'syntax-duplicate-anno';
356
- // do not report again later as anno-duplicate-xyz
357
- } );
358
+ this.addAnnotation( art, '@' + absolute, anno );
358
359
  }
359
- if (!prefix) { // set deprecated $annnotations for cds-lsp
360
+ if (!prefix) { // set deprecated $annotations for cds-lsp
360
361
  if (!art.$annotations)
361
362
  art.$annotations = [];
362
363
  const location = locUtils.combinedLocation( anno.name, anno );
@@ -364,18 +365,53 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant ) {
364
365
  }
365
366
  }
366
367
 
368
+ function addAnnotation( art, prop, anno ) {
369
+ dictAddArray( art, prop, anno, (n, location, a) => {
370
+ // if we would make it a warning, we would still need to keep it an error
371
+ // with '...'; otherwise parse.cdl would have to split annotate statements
372
+ this.error( 'syntax-duplicate-anno', [ location ], { anno: n },
373
+ 'Duplicate assignment with $(ANNO)' );
374
+ a.$errorReported = 'syntax-duplicate-anno';
375
+ // do not report again later as anno-duplicate-xyz
376
+ } );
377
+ }
378
+
379
+ const extensionDicts = { elements: true, enum: true, params: true, returns: true };
380
+
367
381
  function checkExtensionDict( dict ) {
368
382
  for (const name in dict) {
369
383
  const def = dict[name];
370
384
  if (!def.$duplicates)
371
385
  continue;
372
386
 
373
- const numDefines = (def.kind === 'annotate')
374
- ? 0
375
- : def.$duplicates.reduce( addOneForDefinition, addOneForDefinition( 0, def ) );
376
- this.handleExtension( def, name, numDefines );
377
- for (const dup of def.$duplicates)
378
- this.handleExtension( dup, name, numDefines );
387
+ if (def.kind !== 'annotate') {
388
+ const numDefines =
389
+ def.$duplicates.reduce( addOneForDefinition, addOneForDefinition( 0, def ) );
390
+ this.handleDuplicateExtension( def, name, numDefines );
391
+ for (const dup of def.$duplicates)
392
+ this.handleDuplicateExtension( dup, name, numDefines );
393
+ continue;
394
+ }
395
+ // move annotations, 'doc' and 'elements' etc to main member
396
+ for (const dup of def.$duplicates) {
397
+ for (const prop of Object.keys( dup )) {
398
+ if (prop.charAt(0) === '@') {
399
+ this.addAnnotation( def, prop, dup[prop] )
400
+ }
401
+ else if (prop === 'doc') {
402
+ if (def.doc)
403
+ this.warning( 'syntax-duplicate-doc-comment', def.doc.location, {},
404
+ 'Doc comment is overwritten by another one below' );
405
+ def.doc = dup.doc;
406
+ }
407
+ else if (extensionDicts[prop]) {
408
+ if (def[prop])
409
+ this.message( 'syntax-duplicate-annotate', [ def.name.location ], { name, prop } );
410
+ def[prop] = dup[prop]; // continuation semantics: last wins
411
+ }
412
+ }
413
+ }
414
+ def.$duplicates = null;
379
415
  }
380
416
  }
381
417
 
@@ -383,10 +419,15 @@ function addOneForDefinition( count, ext ) {
383
419
  return (ext.kind === 'extend') ? count : count + 1;
384
420
  }
385
421
 
386
- function handleExtension( ext, name, numDefines ) {
387
- if (ext.kind === 'annotate')
388
- this.warning( 'syntax-duplicate-annotate', [ ext.name.location ], { name } );
389
- else if (ext.kind === 'extend')
422
+ /**
423
+ * Handle duplicate extensions. Does not handle `annotate`.
424
+ *
425
+ * @param {XSN.Extension} ext
426
+ * @param {string} name
427
+ * @param {number} numDefines
428
+ */
429
+ function handleDuplicateExtension( ext, name, numDefines ) {
430
+ if (ext.kind === 'extend')
390
431
  this.error( 'syntax-duplicate-extend', [ ext.name.location ],
391
432
  { name, '#': (numDefines ? 'define' : 'extend') } );
392
433
  else if (numDefines === 1)
@@ -434,31 +475,49 @@ function tokenLocation( token, endToken = null ) {
434
475
 
435
476
  // This check is done for performance reason. No need to access a token's
436
477
  // data if we know that it spans only one single line.
437
- const isMultiLineToken = (
438
- endToken.type === this.constructor.DocComment ||
439
- endToken.type === this.constructor.String ||
440
- endToken.type === this.constructor.UnterminatedLiteral
478
+ if (this.isMultiLineToken(token))
479
+ this.fixMultiLineTokenEndLocation(token, loc);
480
+
481
+ return loc;
482
+ }
483
+
484
+ function isMultiLineToken(token) {
485
+ return (
486
+ token.type === this.constructor.DocComment ||
487
+ token.type === this.constructor.String ||
488
+ token.type === this.constructor.UnterminatedLiteral
441
489
  );
442
- if (isMultiLineToken) {
443
- // Count the number of newlines in the token.
444
- const source = endToken.source[1].data;
445
- let newLineCount = 0;
446
- let lastNewlineIndex = endToken.start;
447
- for (let i = endToken.start; i < endToken.stop; i++) {
448
- // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
449
- // because ANTLR only uses LF for line break detection.
450
- if (source[i] === 10) { // code point of '\n'
451
- newLineCount++;
452
- lastNewlineIndex = i;
453
- }
454
- }
455
- if (newLineCount > 0) {
456
- loc.endLine = endToken.line + newLineCount;
457
- loc.endCol = endToken.stop - lastNewlineIndex + 1;
490
+ }
491
+
492
+ /**
493
+ * Adapt end location of `location` according to `token`, assuming that `token` is a multi-line
494
+ * token such as a multi-line string or doc comment.
495
+ *
496
+ * Sets `endLine`/`endCol`, respecting newline characters in the token.
497
+ *
498
+ * @param token
499
+ * @param {CSN.Location} location
500
+ */
501
+ function fixMultiLineTokenEndLocation( token, location ) {
502
+ // Count the number of newlines in the token.
503
+ const source = token.source[1].data;
504
+ let newLineCount = 0;
505
+ let lastNewlineIndex = token.start;
506
+ for (let i = token.start; i < token.stop; i++) {
507
+ // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
508
+ // because ANTLR only uses LF for line break detection.
509
+ if (source[i] === 10) { // code point of '\n'
510
+ newLineCount++;
511
+ lastNewlineIndex = i;
458
512
  }
459
513
  }
460
-
461
- return loc;
514
+ if (newLineCount > 0) {
515
+ location.endLine = token.line + newLineCount;
516
+ location.endCol = token.stop - lastNewlineIndex + 1;
517
+ } else {
518
+ location.endLine = token.line;
519
+ location.endCol = token.stop - token.start + token.column + 2; // after the last char (special for EOF?)
520
+ }
462
521
  }
463
522
 
464
523
  /**
@@ -531,8 +590,8 @@ function docComment( node ) {
531
590
  if (!this.options.docComment)
532
591
  return;
533
592
  if (node.doc) {
534
- this.warning( 'syntax-duplicate-doc-comment', token, {},
535
- 'Repeated doc comment - previous doc is replaced' );
593
+ this.warning( 'syntax-duplicate-doc-comment', node.doc.location, {},
594
+ 'Doc comment is overwritten by another one below' );
536
595
  }
537
596
  node.doc = this.valueWithTokenLocation( parseDocComment( token.text ), token );
538
597
  }
@@ -716,8 +775,7 @@ function quotedLiteral( token, literal ) {
716
775
  literal = token.text.slice( 0, pos - 1 ).toLowerCase();
717
776
  const p = quotedLiteralPatterns[literal] || {};
718
777
 
719
- if ((p.test_fn && !p.test_fn(val) || p.test_re && !p.test_re.test(val)) &&
720
- !this.options.parseOnly)
778
+ if (p.test_fn && !p.test_fn(val) && !this.options.parseOnly)
721
779
  this.warning( 'syntax-invalid-literal', location, { '#': p.test_variant } );
722
780
 
723
781
  if (p.unexpected_char) {