@sap/cds-compiler 2.15.8 → 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 (127) hide show
  1. package/CHANGELOG.md +102 -1590
  2. package/bin/.eslintrc.json +2 -1
  3. package/bin/cdsc.js +61 -46
  4. package/doc/API.md +11 -0
  5. package/doc/CHANGELOG_ARCHIVE.md +1592 -0
  6. package/doc/CHANGELOG_BETA.md +26 -5
  7. package/doc/CHANGELOG_DEPRECATED.md +55 -1
  8. package/doc/{DeprecatedOptions.md → DeprecatedOptions_v2.md} +3 -1
  9. package/doc/Versioning.md +20 -1
  10. package/lib/api/.eslintrc.json +2 -2
  11. package/lib/api/main.js +282 -156
  12. package/lib/api/options.js +17 -88
  13. package/lib/api/validate.js +6 -10
  14. package/lib/base/keywords.js +280 -110
  15. package/lib/base/message-registry.js +85 -25
  16. package/lib/base/messages.js +119 -89
  17. package/lib/base/model.js +46 -2
  18. package/lib/base/optionProcessorHelper.js +53 -21
  19. package/lib/checks/actionsFunctions.js +15 -12
  20. package/lib/checks/annotationsOData.js +1 -1
  21. package/lib/checks/cdsPersistence.js +1 -0
  22. package/lib/checks/elements.js +6 -6
  23. package/lib/checks/invalidTarget.js +1 -1
  24. package/lib/checks/nonexpandableStructured.js +1 -1
  25. package/lib/checks/queryNoDbArtifacts.js +2 -1
  26. package/lib/checks/selectItems.js +101 -15
  27. package/lib/checks/types.js +7 -8
  28. package/lib/checks/utils.js +2 -2
  29. package/lib/checks/validator.js +3 -3
  30. package/lib/compiler/assert-consistency.js +78 -21
  31. package/lib/compiler/base.js +6 -4
  32. package/lib/compiler/builtins.js +177 -10
  33. package/lib/compiler/checks.js +1 -1
  34. package/lib/compiler/define.js +28 -23
  35. package/lib/compiler/extend.js +75 -18
  36. package/lib/compiler/finalize-parse-cdl.js +25 -18
  37. package/lib/compiler/index.js +27 -11
  38. package/lib/compiler/moduleLayers.js +7 -0
  39. package/lib/compiler/populate.js +26 -39
  40. package/lib/compiler/propagator.js +12 -7
  41. package/lib/compiler/resolve.js +207 -236
  42. package/lib/compiler/shared.js +100 -93
  43. package/lib/compiler/tweak-assocs.js +13 -20
  44. package/lib/compiler/utils.js +20 -6
  45. package/lib/edm/annotations/preprocessAnnotations.js +12 -13
  46. package/lib/edm/csn2edm.js +35 -37
  47. package/lib/edm/edm.js +22 -13
  48. package/lib/edm/edmAnnoPreprocessor.js +349 -0
  49. package/lib/edm/edmInboundChecks.js +85 -0
  50. package/lib/edm/edmPreprocessor.js +338 -689
  51. package/lib/edm/edmUtils.js +97 -67
  52. package/lib/gen/Dictionary.json +29 -9
  53. package/lib/gen/language.checksum +1 -1
  54. package/lib/gen/language.interp +8 -31
  55. package/lib/gen/language.tokens +105 -114
  56. package/lib/gen/languageLexer.interp +1 -34
  57. package/lib/gen/languageLexer.js +892 -1007
  58. package/lib/gen/languageLexer.tokens +95 -106
  59. package/lib/gen/languageParser.js +20629 -22474
  60. package/lib/inspect/.eslintrc.json +4 -0
  61. package/lib/inspect/index.js +14 -0
  62. package/lib/inspect/inspectModelStatistics.js +81 -0
  63. package/lib/inspect/inspectPropagation.js +189 -0
  64. package/lib/inspect/inspectUtils.js +44 -0
  65. package/lib/json/from-csn.js +74 -69
  66. package/lib/json/to-csn.js +17 -14
  67. package/lib/language/antlrParser.js +2 -2
  68. package/lib/language/docCommentParser.js +61 -38
  69. package/lib/language/errorStrategy.js +52 -40
  70. package/lib/language/genericAntlrParser.js +424 -292
  71. package/lib/language/language.g4 +604 -687
  72. package/lib/language/multiLineStringParser.js +14 -42
  73. package/lib/language/textUtils.js +44 -0
  74. package/lib/main.d.ts +28 -42
  75. package/lib/main.js +104 -81
  76. package/lib/model/api.js +1 -1
  77. package/lib/model/csnRefs.js +57 -30
  78. package/lib/model/csnUtils.js +189 -287
  79. package/lib/model/revealInternalProperties.js +32 -10
  80. package/lib/model/sortViews.js +32 -31
  81. package/lib/modelCompare/compare.js +3 -0
  82. package/lib/optionProcessor.js +91 -57
  83. package/lib/render/.eslintrc.json +1 -1
  84. package/lib/render/DuplicateChecker.js +4 -7
  85. package/lib/render/manageConstraints.js +70 -2
  86. package/lib/render/toCdl.js +387 -367
  87. package/lib/render/toHdbcds.js +20 -16
  88. package/lib/render/toRename.js +44 -22
  89. package/lib/render/toSql.js +81 -59
  90. package/lib/render/utils/common.js +16 -3
  91. package/lib/render/utils/sql.js +20 -19
  92. package/lib/sql-identifier.js +6 -0
  93. package/lib/transform/db/.eslintrc.json +3 -2
  94. package/lib/transform/db/associations.js +43 -35
  95. package/lib/transform/db/cdsPersistence.js +5 -16
  96. package/lib/transform/db/constraints.js +1 -1
  97. package/lib/transform/db/expansion.js +7 -6
  98. package/lib/transform/db/flattening.js +16 -18
  99. package/lib/transform/db/transformExists.js +7 -5
  100. package/lib/transform/db/views.js +3 -3
  101. package/lib/transform/draft/.eslintrc.json +2 -2
  102. package/lib/transform/draft/db.js +6 -6
  103. package/lib/transform/draft/odata.js +6 -7
  104. package/lib/transform/forHanaNew.js +30 -24
  105. package/lib/transform/forOdataNew.js +14 -16
  106. package/lib/transform/localized.js +35 -25
  107. package/lib/transform/odata/toFinalBaseType.js +10 -10
  108. package/lib/transform/odata/typesExposure.js +17 -8
  109. package/lib/transform/odata/utils.js +1 -38
  110. package/lib/transform/transformUtilsNew.js +63 -77
  111. package/lib/transform/translateAssocsToJoins.js +2 -2
  112. package/lib/transform/universalCsn/.eslintrc.json +2 -2
  113. package/lib/transform/universalCsn/coreComputed.js +11 -6
  114. package/lib/transform/universalCsn/universalCsnEnricher.js +33 -5
  115. package/lib/utils/file.js +31 -21
  116. package/lib/utils/moduleResolve.js +0 -1
  117. package/lib/utils/timetrace.js +20 -21
  118. package/package.json +34 -4
  119. package/share/messages/syntax-expected-integer.md +9 -8
  120. package/doc/ApiMigration.md +0 -237
  121. package/doc/CommandLineMigration.md +0 -58
  122. package/doc/ErrorMessages.md +0 -175
  123. package/doc/FioriAnnotations.md +0 -94
  124. package/doc/ODataTransformation.md +0 -273
  125. package/lib/backends.js +0 -529
  126. package/lib/checks/unknownMagic.js +0 -41
  127. package/lib/fix_antlr4-8_warning.js +0 -56
@@ -7,12 +7,13 @@
7
7
  'use strict';
8
8
 
9
9
  const antlr4 = require('antlr4');
10
- const { ATNState } = require('antlr4/atn/ATNState');
10
+ const { ATNState } = require('antlr4/src/antlr4/atn/ATNState');
11
11
  const { dictAdd, dictAddArray } = require('../base/dictionaries');
12
12
  const locUtils = require('../base/location');
13
13
  const { parseDocComment } = require('./docCommentParser');
14
14
  const { parseMultiLineStringLiteral } = require('./multiLineStringParser');
15
- const { functionsWithoutParens, specialFunctions } = require('../compiler/builtins');
15
+ const { functionsWithoutParens, specialFunctions, quotedLiteralPatterns } = require('../compiler/builtins');
16
+ const { pathName } = require("../compiler/utils");
16
17
 
17
18
  const $location = Symbol.for('cds.$location');
18
19
 
@@ -32,118 +33,91 @@ function _message( parser, severity, id, loc, ...args ) {
32
33
  // this.<function>(...)
33
34
  // in the actions inside the grammar.
34
35
  //
35
- function GenericAntlrParser( ...args ) {
36
- // ANTLR restriction: we cannot add parameters to the constructor.
37
- antlr4.Parser.call( this, ...args );
38
- this.buildParseTrees = false;
39
-
40
- // Common properties.
41
- // We set them here so that they are available in the prototype.
42
- // This improved performance by 25% for certain scenario tests.
43
- // Probably because there was no need to look up the prototype chain anymore.
44
- this.$adaptExpectedToken = null;
45
- this.$adaptExpectedExcludes = [ ];
46
- this.$nextTokensToken = null;
47
- this.$nextTokensContext = null;
48
-
49
- this.prepareGenericKeywords();
50
- this.options = {};
51
-
52
- return this;
53
- }
54
-
55
- // When we define this class with the ES6 `class` syntax, we get
56
- // TypeError: Class constructors cannot be invoked without 'new'
57
- // Reason: the generated ANTLR constructor calls its super constructor via
58
- // old-style `<super>.call(this,...)`, not via `super(...)`.
59
-
60
- GenericAntlrParser.prototype = Object.assign(
61
- Object.create( antlr4.Parser.prototype ), {
62
- message: function(...args) { return _message( this, 'message', ...args ); },
63
- error: function(...args) { return _message( this, 'error', ...args ); },
64
- warning: function(...args) { return _message( this, 'warning', ...args ); },
65
- info: function(...args) { return _message( this, 'info', ...args ); },
66
- attachLocation,
67
- startLocation,
68
- tokenLocation,
69
- valueWithTokenLocation,
70
- previousTokenAtLocation,
71
- combinedLocation,
72
- createDict,
73
- setDictEndLocation,
74
- surroundByParens,
75
- unaryOpForParens,
76
- leftAssocBinaryOp,
77
- classifyImplicitName,
78
- fragileAlias,
79
- identAst,
80
- functionAst,
81
- valuePathAst,
82
- signedExpression,
83
- numberLiteral,
84
- quotedLiteral,
85
- pathName,
86
- docComment,
87
- addDef,
88
- addItem,
89
- artifactForElementAnnotateOrExtend,
90
- assignProps,
91
- createPrefixOp,
92
- setOnce,
93
- setMaxCardinality,
94
- pushIdent,
95
- handleComposition,
96
- associationInSelectItem,
97
- reportExpandInline,
98
- checkTypeFacet,
99
- notSupportedYet,
100
- csnParseOnly,
101
- disallowElementExtension,
102
- noAssignmentInSameLine,
103
- noSemicolonHere,
104
- setLocalToken,
105
- setLocalTokenIfBefore,
106
- excludeExpected,
107
- isStraightBefore,
108
- meltKeywordToIdentifier,
109
- prepareGenericKeywords,
110
- parseMultiLineStringLiteral,
111
- constructor: GenericAntlrParser, // keep this last
112
- }
113
- );
114
-
115
- // Patterns for literal token tests and creation. The value is a map from the
116
- // `prefix` argument of function `quotedliteral` to the following properties:
117
- // - `test_msg`: error message which is issued if `test_fn` or `test_re` fail.
118
- // - `test_fn`: function called with argument `value`, fails falsy return value
119
- // - `test_re`: regular expression, fails if it does not match argument `value`
120
- // - `unexpected_msg`: error message which is issued if `unexpected_char` matches
121
- // - `unexpected_char`: regular expression matching an illegal character in `value`,
122
- // the error location is only correct for a literal <prefix>'<value>'
123
- // - `literal`: the value which is used instead of `prefix` in the AST
124
- // - `normalized`: function called with argument `value`, return value is used
125
- // instead of `value` in the AST
126
- // TODO: think about laxer regexp for date/time/timestamp - normalization?
127
- const quotedLiteralPatterns = {
128
- x: {
129
- test_msg: 'A binary literal must have an even number of characters',
130
- test_fn: (str => Number.isInteger(str.length / 2)),
131
- unexpected_msg: 'A binary literal must only contain characters 0-9, a-f and A-F',
132
- unexpected_char: /[^0-9a-f]/i,
133
- },
134
- time: {
135
- test_msg: 'Expected time\'HH:MM:SS\' where H, M and S are numbers and \':SS\' is optional',
136
- test_re: /^[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?$/,
137
- },
138
- date: {
139
- test_msg: 'Expected date\'YYYY-MM-DD\' where Y, M and D are numbers',
140
- test_re: /^[0-9]{1,4}-[0-9]{1,2}-[0-9]{1,2}$/,
141
- },
142
- timestamp: {
143
- test_msg: 'Expected timestamp\'YYYY-MM-DD HH:MM:SS.u…u\' where Y, M, D, H, S and u are numbers (optional 1-7×u)',
144
- test_re: /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]{1,7})?)?$/,
145
- },
146
- };
36
+ class GenericAntlrParser extends antlr4.Parser {
37
+ constructor( ...args ) {
38
+ // ANTLR restriction: we cannot add parameters to the constructor.
39
+ super( ...args );
40
+ this.buildParseTrees = false;
41
+
42
+ // Common properties.
43
+ // We set them here so that they are available in the prototype.
44
+ // This improved performance by 25% for certain scenario tests.
45
+ // Probably because there was no need to look up the prototype chain anymore.
46
+ this.$adaptExpectedToken = null;
47
+ this.$adaptExpectedExcludes = [ ];
48
+ this.$nextTokensToken = null;
49
+ this.$nextTokensContext = null;
50
+
51
+ this.options = {};
52
+
53
+ this.genericFunctionsStack = [];
54
+ this.$genericKeywords = specialFunctions[''][1];
55
+ }
56
+ }
57
+
58
+ // TODO: Use actual methods.
59
+ Object.assign(GenericAntlrParser.prototype, {
60
+ message: function(...args) { return _message( this, 'message', ...args ); },
61
+ error: function(...args) { return _message( this, 'error', ...args ); },
62
+ warning: function(...args) { return _message( this, 'warning', ...args ); },
63
+ info: function(...args) { return _message( this, 'info', ...args ); },
64
+ attachLocation,
65
+ assignAnnotation,
66
+ addAnnotation,
67
+ checkExtensionDict,
68
+ handleDuplicateExtension,
69
+ startLocation,
70
+ tokenLocation,
71
+ isMultiLineToken,
72
+ fixMultiLineTokenEndLocation,
73
+ valueWithTokenLocation,
74
+ previousTokenAtLocation,
75
+ combinedLocation,
76
+ surroundByParens,
77
+ unaryOpForParens,
78
+ leftAssocBinaryOp,
79
+ classifyImplicitName,
80
+ fragileAlias,
81
+ identAst,
82
+ functionAst,
83
+ setLastAsXpr,
84
+ xprToken,
85
+ valuePathAst,
86
+ signedExpression,
87
+ numberLiteral,
88
+ quotedLiteral,
89
+ pathName,
90
+ docComment,
91
+ addDef,
92
+ addItem,
93
+ addExtension,
94
+ createSource,
95
+ createDict,
96
+ createArray,
97
+ finalizeDictOrArray,
98
+ createPrefixOp,
99
+ setOnce,
100
+ setMaxCardinality,
101
+ pushIdent,
102
+ handleComposition,
103
+ associationInSelectItem,
104
+ reportExpandInline,
105
+ checkTypeFacet,
106
+ notSupportedYet,
107
+ csnParseOnly,
108
+ disallowElementExtension,
109
+ noAssignmentInSameLine,
110
+ noSemicolonHere,
111
+ setLocalToken,
112
+ setLocalTokenIfBefore,
113
+ setLocalTokenForId,
114
+ excludeExpected,
115
+ isStraightBefore,
116
+ meltKeywordToIdentifier,
117
+ prepareGenericKeywords,
118
+ reportErrorForGenericKeyword,
119
+ parseMultiLineStringLiteral,
120
+ });
147
121
 
148
122
  // Use the following function for language constructs which we (currently)
149
123
  // just being able to parse, in able to run tests from HANA CDS. As soon as we
@@ -163,8 +137,8 @@ function notSupportedYet( text, ...tokens ) {
163
137
  }
164
138
 
165
139
  // Use the following function for language constructs which we (currently) do
166
- // not really compile, just use to produce a CSN for functions parseToCqn() and
167
- // parseToExpr().
140
+ // not really compile, just use to produce a CSN for functions parse.cql() and
141
+ // parse.expr().
168
142
  function csnParseOnly( text, ...tokens ) {
169
143
  if (!text || this.options.parseOnly)
170
144
  return;
@@ -218,6 +192,15 @@ function setLocalTokenIfBefore( string, tokenName, before, inSameLine ) {
218
192
  ll1.type = this.constructor[tokenName];
219
193
  }
220
194
 
195
+ function setLocalTokenForId( tokenNameMap ) {
196
+ const tokenName = tokenNameMap[ this._input.LT(2).text || '' ];
197
+ const ll1 = this.getCurrentToken();
198
+ if (tokenName &&
199
+ (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )))
200
+ ll1.type = this.constructor[tokenName];
201
+ return !!tokenName;
202
+ }
203
+
221
204
  // // Special function for rule `requiredSemi` before return $ctx
222
205
  // function braceForSemi() {
223
206
  // if (RBRACE == null)
@@ -275,24 +258,47 @@ function meltKeywordToIdentifier( exceptTrueFalseNull = false ) {
275
258
  token.type = Identifier;
276
259
  }
277
260
 
278
- function prepareGenericKeywords( pathItem ) {
279
- this.$genericKeywords = { argFull: [] };
280
- if (!pathItem)
281
- return;
282
- const func = pathItem.id && specialFunctions[pathItem.id.toUpperCase()];
283
- const spec = func && func[pathItem.args ? pathItem.args.length : 0];
284
- if (!spec)
285
- return;
286
- // currently, we only have 'argFull', i.e. a keyword which is alternative to expression
287
- // TODO: If not just at the beginning, we need a stack for $genericKeywords,
288
- // as we can have nested special functions
289
- this.$genericKeywords.argFull = Object.keys( spec );
261
+ const genericTokenTypes = {
262
+ expr: 'GenericExpr',
263
+ separator: 'GenericSeparator',
264
+ intro: 'GenericIntro',
265
+ };
266
+
267
+ function prepareGenericKeywords( pathItem, expected = null) {
268
+ const length = pathItem?.args?.length || 0;
269
+ const argPos = (expected ? length -1 : length);
270
+ const func = pathItem?.id && specialFunctions[pathItem.id.toUpperCase()];
271
+ const spec = func && func[argPos] || specialFunctions[''][argPos ? 1 : 0];
272
+ this.$genericKeywords = spec;
290
273
  // @ts-ignore
291
274
  const token = this.getCurrentToken() || { text: '' };
292
- if (spec[token.text.toUpperCase()] === 'argFull') {
293
- // @ts-ignore
294
- token.type = this.constructor.GenericArgFull;
275
+ const text = token.text.toUpperCase();
276
+ let generic = spec[text];
277
+ // console.log('PGK:',token.text,generic,expected,spec,func,argPos)
278
+ if (expected) { // 'separator' or 'expr' (after 'separator')
279
+ if (generic !== expected)
280
+ return;
281
+ }
282
+ else if (!generic || generic === 'separator') {
283
+ // Mismatch at beginning (or just an expression): keep token type
284
+ // (if not expression, issue error and consider the token to be an
285
+ // expression replacement, like ALL)
286
+ return;
287
+ }
288
+ else if (generic === 'expr' && spec.intro && spec.intro.includes( text )) {
289
+ // token is both an intro and an expression, like LEADING for TRIM
290
+ const next = this._input.LT(2).text;
291
+ if (!next || // followed by EOF -> consider it to be 'intro', better for CC
292
+ next !== ',' && next !== ')' && spec[next.toUpperCase()] !== 'separator')
293
+ generic = 'intro'; // is intro if next token is not separator, not ',', ')'
295
294
  }
295
+ // @ts-ignore
296
+ token.type = this.constructor[genericTokenTypes[generic]];
297
+ }
298
+ // To be called before having matched ( HideAlternatives | … )
299
+ function reportErrorForGenericKeyword() {
300
+ this._errHandler.reportUnwantedToken( this );
301
+ //this._errHandler.reportInputMismatch( this, { offending: this._input.LT(1) }, null );
296
302
  }
297
303
 
298
304
  // Attach location matched by current rule to node `art`. If a location is
@@ -302,14 +308,133 @@ function prepareGenericKeywords( pathItem ) {
302
308
  function attachLocation( art ) {
303
309
  if (!art || art.$parens)
304
310
  return art;
305
- if (!art.location)
306
- art.location = this.startLocation();
307
- const { stop } = this._ctx;
308
- art.location.endLine = stop.line;
309
- 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
+
310
327
  return art;
311
328
  }
312
329
 
330
+ function assignAnnotation( art, anno, prefix = '', iHaveVariant = false ) {
331
+ const { name, $flatten } = anno;
332
+ const { path } = name;
333
+ if (path.broken || !path[path.length - 1].id)
334
+ return;
335
+ const pathname = pathName( path );
336
+ let absolute = '';
337
+ if (name.variant) {
338
+ const variant = pathName( name.variant.path );
339
+ absolute = `${ prefix }${ pathname }#${ variant }`;
340
+ if (iHaveVariant) { // TODO: do we really care in the parser / core compiler?
341
+ this.error( 'anno-duplicate-variant', [ name.variant.location ],
342
+ {}, // TODO: params
343
+ 'Annotation variant has been already provided' );
344
+ }
345
+ }
346
+ else if (!prefix || pathname !== '$value') {
347
+ absolute = `${ prefix }${ pathname }`;
348
+ }
349
+ else {
350
+ absolute = prefix.slice( 0, -1 );
351
+ }
352
+ if ($flatten) {
353
+ for (const a of $flatten)
354
+ this.assignAnnotation( art, a, `${ absolute }.`, iHaveVariant || name.variant);
355
+ }
356
+ else {
357
+ name.absolute = absolute;
358
+ this.addAnnotation( art, '@' + absolute, anno );
359
+ }
360
+ if (!prefix) { // set deprecated $annotations for cds-lsp
361
+ if (!art.$annotations)
362
+ art.$annotations = [];
363
+ const location = locUtils.combinedLocation( anno.name, anno );
364
+ art.$annotations.push( { value: anno, location } );
365
+ }
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
+
381
+ function checkExtensionDict( dict ) {
382
+ for (const name in dict) {
383
+ const def = dict[name];
384
+ if (!def.$duplicates)
385
+ continue;
386
+
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;
415
+ }
416
+ }
417
+
418
+ function addOneForDefinition( count, ext ) {
419
+ return (ext.kind === 'extend') ? count : count + 1;
420
+ }
421
+
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')
431
+ this.error( 'syntax-duplicate-extend', [ ext.name.location ],
432
+ { name, '#': (numDefines ? 'define' : 'extend') } );
433
+ else if (numDefines === 1)
434
+ ext.$errorReported = 'syntax-duplicate-extend'; // a definition, but not duplicate
435
+ }
436
+
437
+
313
438
  /**
314
439
  * Return start location of `token`, or the first token matched by the current
315
440
  * rule if `token` is undefined
@@ -350,31 +475,49 @@ function tokenLocation( token, endToken = null ) {
350
475
 
351
476
  // This check is done for performance reason. No need to access a token's
352
477
  // data if we know that it spans only one single line.
353
- const isMultiLineToken = (
354
- endToken.type === this.constructor.DocComment ||
355
- endToken.type === this.constructor.String ||
356
- 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
357
489
  );
358
- if (isMultiLineToken) {
359
- // Count the number of newlines in the token.
360
- const source = endToken.source[1].data;
361
- let newLineCount = 0;
362
- let lastNewlineIndex = endToken.start;
363
- for (let i = endToken.start; i < endToken.stop; i++) {
364
- // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
365
- // because ANTLR only uses LF for line break detection.
366
- if (source[i] === 10) { // code point of '\n'
367
- newLineCount++;
368
- lastNewlineIndex = i;
369
- }
370
- }
371
- if (newLineCount > 0) {
372
- loc.endLine = endToken.line + newLineCount;
373
- 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;
374
512
  }
375
513
  }
376
-
377
- 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
+ }
378
521
  }
379
522
 
380
523
  /**
@@ -409,20 +552,6 @@ function combinedLocation( start, end ) {
409
552
  return locUtils.combinedLocation( start, end );
410
553
  }
411
554
 
412
- function createDict( location = null ) {
413
- const dict = Object.create(null);
414
- dict[$location] = location || this.startLocation( this._input.LT(-1) );
415
- return dict;
416
- }
417
-
418
- function setDictEndLocation( dict ) {
419
- const stop = this._input.LT(-1);
420
- Object.assign( dict[$location], {
421
- endLine: stop.line,
422
- endCol: stop.stop - stop.start + stop.column + 2,
423
- } );
424
- }
425
-
426
555
  function surroundByParens( expr, open, close, asQuery = false ) {
427
556
  if (!expr)
428
557
  return expr;
@@ -435,7 +564,7 @@ function surroundByParens( expr, open, close, asQuery = false ) {
435
564
  }
436
565
 
437
566
  function unaryOpForParens( query, val ) {
438
- const parens = query.$parens;
567
+ const parens = query?.$parens;
439
568
  if (!parens)
440
569
  return query;
441
570
  const location = parens[parens.length - 1];
@@ -461,8 +590,8 @@ function docComment( node ) {
461
590
  if (!this.options.docComment)
462
591
  return;
463
592
  if (node.doc) {
464
- this.warning( 'syntax-duplicate-doc-comment', token, {},
465
- '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' );
466
595
  }
467
596
  node.doc = this.valueWithTokenLocation( parseDocComment( token.text ), token );
468
597
  }
@@ -488,10 +617,11 @@ function fragileAlias( ast, safe = false ) {
488
617
  }
489
618
 
490
619
  // Return AST for identifier token `token`. Also check that identifer is not empty.
491
- function identAst( token, category ) {
620
+ function identAst( token, category, noTokenTypeCheck = false ) {
492
621
  token.isIdentifier = category;
493
622
  let id = token.text;
494
- if (token.type !== this.constructor.Identifier && !/^[a-zA-Z]+$/.test( id ))
623
+ if (!noTokenTypeCheck &&
624
+ token.type !== this.constructor.Identifier && !/^[a-zA-Z_]+$/.test( id ))
495
625
  id = '';
496
626
  if (token.text[0] === '!') {
497
627
  id = id.slice( 2, -1 ).replace( /]]/g, ']' );
@@ -518,11 +648,11 @@ function identAst( token, category ) {
518
648
  return { id, $delimited: true, location: this.tokenLocation( token ) };
519
649
  }
520
650
 
521
- function functionAst( token, xprToken ) {
651
+ function functionAst( token, xpr ) { // only for special function TRIM and EXTRACT
522
652
  // TODO: XSN func cleanup
523
653
  const location = this.tokenLocation( token );
524
- const args = xprToken
525
- ? [ { op: { location, val: 'xpr' }, args: [], location: this.tokenLocation( xprToken ) } ]
654
+ const args = xpr
655
+ ? [ { op: { location, val: 'ixpr' }, args: [], location: this.tokenLocation( xpr ) } ]
526
656
  : [];
527
657
  return {
528
658
  op: { location, val: 'call' },
@@ -532,6 +662,21 @@ function functionAst( token, xprToken ) {
532
662
  };
533
663
  }
534
664
 
665
+ function setLastAsXpr( args ) {
666
+ const pos = args.length - 1;
667
+ const last = args[pos];
668
+ if (!last || last?.op?.val === 'ixpr') // actuall an internal xpr, which is always flattened
669
+ return last;
670
+ const location = { ...last.location };
671
+ args[pos] = { op: { location, val: 'ixpr' }, args: [ last ], location };
672
+ return args[pos].args;
673
+ }
674
+
675
+ function xprToken() {
676
+ const token = this._input.LT(-1);
677
+ return { location: this.tokenLocation( token ), val: token.text, literal: 'token' }
678
+ }
679
+
535
680
  function valuePathAst( ref ) {
536
681
  // TODO: XSN representation of functions is a bit strange - rework if methods
537
682
  // are introduced
@@ -630,21 +775,19 @@ function quotedLiteral( token, literal ) {
630
775
  literal = token.text.slice( 0, pos - 1 ).toLowerCase();
631
776
  const p = quotedLiteralPatterns[literal] || {};
632
777
 
633
- // TODO: make tests available for CSN parser
634
- if ((p.test_fn && !p.test_fn(val) || p.test_re && !p.test_re.test(val)) &&
635
- !this.options.parseOnly)
636
- this.error( null, location, p.test_msg ); // TODO: message id
778
+ if (p.test_fn && !p.test_fn(val) && !this.options.parseOnly)
779
+ this.warning( 'syntax-invalid-literal', location, { '#': p.test_variant } );
637
780
 
638
781
  if (p.unexpected_char) {
639
782
  const idx = val.search(p.unexpected_char);
640
783
  if (~idx) {
641
- this.error( null, { // TODO: message id
784
+ this.warning( 'syntax-invalid-literal', {
642
785
  file: location.file,
643
786
  line: location.line,
644
787
  endLine: location.line,
645
788
  col: atChar(idx),
646
789
  endCol: atChar( idx + (val[idx] === '\'' ? 2 : 1) ),
647
- }, p.unexpected_msg );
790
+ }, { '#': p.unexpected_variant } );
648
791
  }
649
792
  }
650
793
  return {
@@ -659,10 +802,6 @@ function quotedLiteral( token, literal ) {
659
802
  }
660
803
  }
661
804
 
662
- function pathName( path, brokenName ) {
663
- return (path && !path.broken) ? path.map( id => id.id ).join('.') : brokenName;
664
- }
665
-
666
805
  function pushIdent( path, ident, prefix ) {
667
806
  if (!ident) {
668
807
  path.broken = true;
@@ -691,61 +830,62 @@ function pushIdent( path, ident, prefix ) {
691
830
  }
692
831
  }
693
832
 
694
- // Add new definition to dictionary property `env` of node `parent` and return
695
- // that definition. Also attach the following properties to the new definition:
696
- // - `name`: argument `name`, which is used as key in the dictionary
697
- // - `kind`: argument `kind` if that is truthy
698
- // - `location`: argument `location` or the start location of source matched by
699
- // current rule
700
- // - properties in argument `props` which are no empty (undefined, null, {},
701
- // []), ANTLR tokens are replaced by their locations
833
+ // Add new definition `art` to dictionary property `env` of node `parent`.
834
+ // Return `art`.
702
835
  //
703
- // Hack: if argument `location` is exactly `true`, do not set `location`
704
- // (except if part of `props`), but also include the empty properties of
705
- // `props`.
706
- function addDef( parent, env, kind, name, annos, props, location ) {
836
+ // If argument `kind` is provided, set `art.kind` to that value.
837
+ // If argument `name` is provided, set `art.name`:
838
+ // - if `name` is an array, the name consist of the ID of the last path item
839
+ // (for elements via columns, foreign keys, table aliases)
840
+ // - if `name` is an object, the name consist of the IDs of all path items
841
+ // (for main artifact definitions)
842
+ function addDef( art, parent, env, kind, name ) {
707
843
  if (Array.isArray(name)) {
708
844
  // XSN TODO: clearly say: definitions have name.path, members have name.id
709
845
  const last = name.length && name[name.length - 1];
710
846
  if (last && last.id) { // // A.B.C -> 'C'
711
- name = {
847
+ art.name = {
712
848
  id: last.id, location: last.location, $inferred: 'as',
713
849
  };
714
850
  }
715
851
  }
716
- else if (name && name.id == null) {
717
- name.id = pathName( name.path ); // A.B.C -> 'A.B.C'
852
+ else if (name) {
853
+ art.name = name;
854
+ if (name.id == null)
855
+ name.id = pathName( name.path ); // A.B.C -> 'A.B.C'
856
+ // TODO: get rid of setting `id`, only use for named values in structs
718
857
  }
719
- const art = this.assignProps( { name }, annos, props, location );
858
+
720
859
  if (kind)
721
860
  art.kind = kind;
722
- if (!parent[env]) // TODO: dump with --test-mode, env !== 'artifacts'
723
- parent[env] = env === 'args' ? Object.create(null) : this.createDict( { ...location } );
724
861
  if (!art.name || art.name.id == null) {
725
862
  // no id was parsed, but with error recovery: no further error
726
863
  // TODO: add to parent[env]['']
727
864
  // which could be tested in name search (then no undefined-ref error)
865
+ // console.log( kind+': !!', art, parent, env, name )
728
866
  return art;
729
867
  }
730
- else if (env === 'artifacts' || env === 'vocabularies') {
868
+ // console.log( kind+':', art, parent, env, name )
869
+
870
+ if (env === 'artifacts' || env === 'vocabularies') {
731
871
  dictAddArray( parent[env], art.name.id, art );
732
872
  }
733
873
  else if (kind || this.options.parseOnly) {
734
874
  dictAdd( parent[env], art.name.id, art );
735
875
  }
736
876
  else {
737
- dictAdd( parent[env], art.name.id, art, ( name, loc ) => {
877
+ dictAdd( parent[env], art.name.id, art, ( duplicateName, loc ) => {
738
878
  // do not use function(), otherwise `this` is wrong:
739
879
  if (kind === 0) {
740
- this.error( 'duplicate-argument', loc, { name },
880
+ this.error( 'duplicate-argument', loc, { name: duplicateName },
741
881
  'Duplicate value for parameter $(NAME)' );
742
882
  }
743
883
  else if (kind === '') {
744
- this.error( 'duplicate-excluding', loc, { name, keyword: 'excluding' },
884
+ this.error( 'duplicate-excluding', loc, { name: duplicateName, keyword: 'excluding' },
745
885
  'Duplicate $(NAME) in the $(KEYWORD) clause' );
746
886
  }
747
887
  else {
748
- this.error( 'duplicate-prop', loc, { name },
888
+ this.error( 'duplicate-prop', loc, { name: duplicateName },
749
889
  'Duplicate value for structure property $(NAME)' );
750
890
  }
751
891
  } );
@@ -753,85 +893,78 @@ function addDef( parent, env, kind, name, annos, props, location ) {
753
893
  return art;
754
894
  }
755
895
 
756
- // Add new definition to array property `env` of node `parent` and return
757
- // that definition. Also attach the following properties to the new definition:
758
- // - `kind`: argument `kind` if that is truthy
759
- // - `location`: argument `location` or the start location of source matched by
760
- // current rule
761
- // - properties in argument `props` which are no empty (undefined, null, {},
762
- // []); ANTLR tokens are replaced by their locations
763
- //
764
- // Hack: if argument `location` is exactly `true`, do not set `location`
765
- // (except if part of `props`), but also include the empty properties of
766
- // `props`.
767
- function addItem( parent, env, kind, annos, props, location ) {
768
- const art = this.assignProps( {}, annos, props, location );
769
- if (kind)
770
- art.kind = kind;
771
- if (!env)
772
- parent.push( art );
773
- else if (!parent[env])
774
- parent[env] = [ art ];
775
- else
776
- parent[env].push( art );
896
+ // Add new definition `art` to array property `env` of node `parent`.
897
+ // Also set `kind`. Returns `art`.
898
+ function addItem( art, parent, env, kind ) {
899
+ art.kind = kind;
900
+ parent[env].push( art );
777
901
  return art;
778
902
  }
779
-
780
903
  /**
781
- * For `annotate/extend E:elem.sub`, create the `elements` structure
782
- * that can be used by the core compiler to annotate/extend elements.
904
+ * Add `annotate/extend Main.Artifact:elem.sub` to `‹xsn›.extensions`:
905
+ * - the array item is an extend/annotate for `Main.Artifact`,
906
+ * - for each path item in `elem.sub`, we add an `elements` property containing
907
+ * one extend/annotate for the corresponding element
908
+ * - The deepest extend/annotate is the object which is to be extended
783
909
  *
784
- * @param {string} kind Either `annotate` or `extend`
785
- * @param {object} artifact Main artifact that shall have `elements`.
786
- * @param {XSN.Path} elementPath Path as returned by `simplePath` token.
787
- * @param {object[]} annos Existing annotations that shall be added to the _last_ path step.
788
- * @param {XSN.Location} artifactLocation Start location of the `annotate` statement.
789
- * @returns {object} Deepest element
790
- */
791
- function artifactForElementAnnotateOrExtend(kind, artifact, elementPath, annos, artifactLocation ) {
792
- if (!Array.isArray(elementPath) || elementPath.broken || elementPath.length < 1)
793
- return artifact;
794
-
795
- for (const seg of elementPath.slice(0, -1)) {
796
- artifact = this.addDef( artifact, 'elements', kind,
797
- { path: [seg], location: seg.location }, null, {}, artifactLocation );
910
+ * @param {object} ext The object containing the location and annotations for the extension.
911
+ * @param {object} parent The parent containing the `extensions` property, i.e. the source.
912
+ * @param {string} kind Either `annotate` or `extend`.
913
+ * @param {object} artName The "name object" for `Main.Artifact`.
914
+ * @param {XSN.Path} elemPath Path as returned by `simplePath` rule.
915
+ */
916
+ function addExtension( ext, parent, kind, artName, elemPath ) {
917
+ const { location } = ext;
918
+ if (!Array.isArray( elemPath ) || !elemPath.length || elemPath.broken) {
919
+ ext.name = artName;
920
+ this.addItem( ext, parent, 'extensions', kind );
921
+ return;
922
+ }
923
+ // Note: the element extensions share a common `location`, also with the
924
+ // extension of the main artifact; its end location will usually set later
925
+ parent = this.addItem( { name: artName, location }, parent, 'extensions', kind );
926
+
927
+ const last = elemPath[elemPath.length - 1];
928
+ for (const seg of elemPath) {
929
+ parent.elements = Object.create(null); // no dict location → no createDict()
930
+ parent = this.addDef( (seg === last ? ext : { location }),
931
+ parent, 'elements', kind, seg );
798
932
  }
799
- const last = elementPath[elementPath.length - 1];
800
- artifact = this.addDef( artifact, 'elements', kind,
801
- { path: [ last ], location: last.location }, annos, {}, artifactLocation );
802
- return artifact;
803
933
  }
804
934
 
805
- /** Assign all non-empty (undefined, null, {}, []) properties in argument
806
- * `props` and argument `annos` as property `$annotations` to `target`
807
- * and return it. Hack: if argument `annos` is exactly `true`, return
808
- * `Object.assign( target, props )`, for rule `namedValue`. ANTLR tokens are
809
- * replaced by their locations.
810
- *
811
- * @param {any} target
812
- * @param {any[]|true} [annos=[]]
813
- * @param {any} [props]
814
- * @param {any} [location]
815
- */
816
- function assignProps( target, annos = [], props = null, location = null) {
817
- if (annos === true)
818
- return Object.assign( target, props );
819
- target.location = location || this.startLocation( this._ctx.start );
820
- // Object.assign without "empty" elements/properties and with mappings:
821
- // - token instanceof antlr4.CommonToken => location of token
822
- for (const key in props) {
823
- let val = props[key];
824
- if (val instanceof antlr4.CommonToken)
825
- val = this.valueWithTokenLocation( true, val);
826
- // only copy properties which are not undefined, null, {} or []
827
- if (val != null &&
828
- (typeof val !== 'object' ||
829
- (Array.isArray(val) ? val.length : Object.getOwnPropertyNames(val).length) ) )
830
- target[key] = val;
831
- }
832
- if (annos)
833
- target.$annotations = annos;
834
- return target;
935
+ // must be in action directly after having parsed '{' or '(`
936
+ function createDict() {
937
+ const dict = Object.create(null);
938
+ dict[$location] = this.startLocation( this._input.LT(-1) );
939
+ return dict;
940
+ }
941
+
942
+ // must be in action directly after having parsed '[' or '(` or `{`
943
+ function createArray() {
944
+ const array = [];
945
+ array[$location] = this.startLocation( this._input.LT(-1) );
946
+ return array;
947
+ }
948
+
949
+ // must be in action directly after having parsed '}' or ')`
950
+ function finalizeDictOrArray( dict ) {
951
+ const loc = dict[$location];
952
+ if (!loc)
953
+ return;
954
+ const stop = this._input.LT(-1);
955
+ loc.endLine = stop.line;
956
+ loc.endCol = stop.stop - stop.start + stop.column + 2;
957
+ }
958
+
959
+ function createSource() {
960
+ return {
961
+ kind: 'source',
962
+ usings: [],
963
+ dependencies: [],
964
+ artifacts: Object.create(null),
965
+ // vocabularies: Object.create(null),
966
+ extensions: [],
967
+ };
835
968
  }
836
969
 
837
970
  // Create AST node for prefix operator `op` and arguments `args`
@@ -879,14 +1012,12 @@ function setOnce( target, prop, value, ...tokens ) {
879
1012
  target[prop] = value;
880
1013
  }
881
1014
 
882
- function setMaxCardinality( art, token, max, inferred ) {
1015
+ function setMaxCardinality( art, token, max ) {
883
1016
  const location = this.tokenLocation( token );
884
1017
  if (!art.cardinality) {
885
1018
  art.cardinality = { targetMax: Object.assign( { location }, max ), location };
886
- if (inferred)
887
- art.cardinality.$inferred = inferred;
888
1019
  }
889
- else if (!inferred) {
1020
+ else {
890
1021
  this.warning( 'syntax-repeated-cardinality', location, { keyword: token.text },
891
1022
  'The target cardinality has already been specified - ignored $(KEYWORD)' );
892
1023
  }
@@ -905,6 +1036,9 @@ function handleComposition( cardinality, isComposition ) {
905
1036
  }
906
1037
 
907
1038
  function associationInSelectItem( art ) {
1039
+ if (!art.value) // e.g. `expand` without value (for new structures)
1040
+ return;
1041
+
908
1042
  const isPath = art.value.path && art.value.path.length
909
1043
  const isIdentifier = isPath && art.value.path.length === 1;
910
1044
  if (isIdentifier) {
@@ -958,6 +1092,4 @@ function checkTypeFacet( art, argIdent ) {
958
1092
  }
959
1093
  }
960
1094
 
961
- module.exports = {
962
- genericAntlrParser: GenericAntlrParser,
963
- };
1095
+ module.exports = GenericAntlrParser;