@sap/cds-compiler 5.9.4 → 6.0.10

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 (111) hide show
  1. package/CHANGELOG.md +104 -319
  2. package/README.md +1 -1
  3. package/bin/cds_update_identifiers.js +3 -5
  4. package/bin/cdsc.js +22 -8
  5. package/bin/cdshi.js +1 -1
  6. package/bin/cdsse.js +4 -4
  7. package/doc/CHANGELOG_BETA.md +11 -0
  8. package/doc/CHANGELOG_DEPRECATED.md +29 -0
  9. package/lib/api/main.js +8 -5
  10. package/lib/api/options.js +12 -10
  11. package/lib/base/builtins.js +1 -0
  12. package/lib/base/message-registry.js +190 -96
  13. package/lib/base/messages.js +29 -20
  14. package/lib/base/model.js +14 -24
  15. package/lib/checks/actionsFunctions.js +10 -20
  16. package/lib/checks/annotationsOData.js +1 -1
  17. package/lib/checks/elements.js +30 -10
  18. package/lib/checks/enums.js +31 -0
  19. package/lib/checks/foreignKeys.js +2 -2
  20. package/lib/checks/hasPersistedElements.js +5 -0
  21. package/lib/checks/invalidTarget.js +1 -1
  22. package/lib/checks/managedWithoutKeys.js +5 -4
  23. package/lib/checks/queryNoDbArtifacts.js +10 -8
  24. package/lib/checks/types.js +5 -5
  25. package/lib/checks/validator.js +6 -4
  26. package/lib/compiler/assert-consistency.js +12 -9
  27. package/lib/compiler/checks.js +18 -50
  28. package/lib/compiler/define.js +6 -6
  29. package/lib/compiler/extend.js +2 -1
  30. package/lib/compiler/generate.js +14 -17
  31. package/lib/compiler/populate.js +8 -31
  32. package/lib/compiler/propagator.js +21 -35
  33. package/lib/compiler/resolve.js +35 -22
  34. package/lib/compiler/shared.js +7 -1
  35. package/lib/compiler/tweak-assocs.js +1 -1
  36. package/lib/compiler/utils.js +1 -1
  37. package/lib/edm/annotations/edmJson.js +20 -15
  38. package/lib/edm/annotations/genericTranslation.js +7 -8
  39. package/lib/edm/csn2edm.js +46 -50
  40. package/lib/edm/edm.js +8 -7
  41. package/lib/edm/edmPreprocessor.js +33 -83
  42. package/lib/edm/edmUtils.js +2 -2
  43. package/lib/gen/BaseParser.js +55 -44
  44. package/lib/gen/CdlGrammar.checksum +1 -1
  45. package/lib/gen/CdlParser.js +1133 -1150
  46. package/lib/json/from-csn.js +70 -43
  47. package/lib/json/to-csn.js +6 -8
  48. package/lib/language/multiLineStringParser.js +3 -2
  49. package/lib/main.d.ts +58 -24
  50. package/lib/model/csnUtils.js +28 -39
  51. package/lib/model/xprAsTree.js +23 -9
  52. package/lib/modelCompare/compare.js +5 -4
  53. package/lib/optionProcessor.js +21 -17
  54. package/lib/parsers/AstBuildingParser.js +63 -11
  55. package/lib/parsers/XprTree.js +57 -3
  56. package/lib/parsers/identifiers.js +1 -1
  57. package/lib/parsers/index.js +0 -3
  58. package/lib/render/manageConstraints.js +25 -25
  59. package/lib/render/toCdl.js +173 -170
  60. package/lib/render/toHdbcds.js +126 -128
  61. package/lib/render/toRename.js +7 -7
  62. package/lib/render/toSql.js +128 -125
  63. package/lib/render/utils/common.js +47 -22
  64. package/lib/render/utils/delta.js +25 -25
  65. package/lib/render/utils/operators.js +2 -2
  66. package/lib/render/utils/pretty.js +5 -5
  67. package/lib/render/utils/sql.js +13 -13
  68. package/lib/render/utils/standardDatabaseFunctions.js +115 -103
  69. package/lib/render/utils/unique.js +4 -4
  70. package/lib/transform/db/applyTransformations.js +1 -1
  71. package/lib/transform/db/assertUnique.js +2 -2
  72. package/lib/transform/db/associations.js +6 -7
  73. package/lib/transform/db/assocsToQueries/utils.js +4 -5
  74. package/lib/transform/db/backlinks.js +12 -9
  75. package/lib/transform/db/cdsPersistence.js +8 -7
  76. package/lib/transform/db/constraints.js +13 -10
  77. package/lib/transform/db/expansion.js +7 -3
  78. package/lib/transform/db/flattening.js +4 -14
  79. package/lib/transform/db/processSqlServices.js +2 -1
  80. package/lib/transform/db/temporal.js +5 -7
  81. package/lib/transform/db/views.js +2 -4
  82. package/lib/transform/draft/db.js +8 -8
  83. package/lib/transform/draft/odata.js +10 -7
  84. package/lib/transform/forOdata.js +10 -5
  85. package/lib/transform/forRelationalDB.js +5 -75
  86. package/lib/transform/localized.js +1 -1
  87. package/lib/transform/odata/createForeignKeys.js +11 -10
  88. package/lib/transform/odata/flattening.js +8 -4
  89. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +96 -0
  90. package/lib/transform/odata/typesExposure.js +3 -3
  91. package/lib/transform/transformUtils.js +4 -8
  92. package/lib/transform/translateAssocsToJoins.js +14 -7
  93. package/lib/transform/universalCsn/universalCsnEnricher.js +10 -4
  94. package/lib/utils/objectUtils.js +0 -17
  95. package/package.json +10 -13
  96. package/share/messages/def-upcoming-virtual-change.md +1 -1
  97. package/LICENSE +0 -37
  98. package/bin/cds_remove_invalid_whitespace.js +0 -138
  99. package/doc/CHANGELOG_ARCHIVE.md +0 -3604
  100. package/lib/gen/genericAntlrParser.js +0 -3
  101. package/lib/gen/language.checksum +0 -1
  102. package/lib/gen/language.interp +0 -456
  103. package/lib/gen/language.tokens +0 -180
  104. package/lib/gen/languageLexer.interp +0 -439
  105. package/lib/gen/languageLexer.js +0 -1483
  106. package/lib/gen/languageLexer.tokens +0 -167
  107. package/lib/gen/languageParser.js +0 -24941
  108. package/lib/language/antlrParser.js +0 -205
  109. package/lib/language/errorStrategy.js +0 -646
  110. package/lib/language/genericAntlrParser.js +0 -1572
  111. package/lib/parsers/CdlGrammar.g4 +0 -2070
@@ -1,1572 +0,0 @@
1
- // Generic ANTLR parser class with AST-building functions
2
-
3
- // To have an AST also in the case of syntax errors, produce it by adding
4
- // sub-nodes to a parent node, not by returning sub-ASTs (the latter is fine
5
- // for secondary attachments).
6
-
7
- 'use strict';
8
-
9
- const antlr4 = require('antlr4');
10
- const { ATNState } = require('antlr4/src/antlr4/atn/ATNState');
11
- const { DEFAULT: CommonTokenFactory } = require('antlr4/src/antlr4/CommonTokenFactory');
12
- const { dictAdd, dictAddArray } = require('../base/dictionaries');
13
- const locUtils = require('../base/location');
14
- const { parseDocComment } = require('./docCommentParser');
15
- const { parseMultiLineStringLiteral } = require('./multiLineStringParser');
16
- const {
17
- specialFunctions,
18
- quotedLiteralPatterns,
19
- } = require('../compiler/builtins');
20
- const { functionsWithoutParentheses } = require('../parsers/identifiers');
21
- const { Location } = require('../base/location');
22
- const { pathName } = require('../compiler/utils');
23
- const { XsnArtifact, XsnName, XsnSource } = require('../compiler/xsn-model');
24
- const { isBetaEnabled } = require('../base/model');
25
- const { weakLocation } = require('../base/location');
26
- const { normalizeNewLine, normalizeNumberString } = require('./textUtils');
27
-
28
- const $location = Symbol.for('cds.$location');
29
-
30
- // Push message `msg` with location `loc` to array of errors:
31
- function _message( parser, severity, id, loc, ...args ) {
32
- const msg = parser.$messageFunctions[severity]; // set in antlrParser.js
33
- if (loc instanceof antlr4.CommonToken)
34
- loc = parser.tokenLocation(loc);
35
- return msg( id, loc, ...args );
36
- }
37
-
38
- // Class which is to be used as grammar option with
39
- // grammar <name> options { superclass = genericAntlrParser; }
40
- //
41
- // The individual AST building functions are to be used with
42
- // this.<function>(...)
43
- // in the actions inside the grammar.
44
- //
45
- class GenericAntlrParser extends antlr4.Parser {
46
- constructor( ...args ) {
47
- // ANTLR restriction: we cannot add parameters to the constructor.
48
- super( ...args );
49
- this.buildParseTrees = false;
50
-
51
- // Common properties.
52
- // We set them here so that they are available in the prototype.
53
- // This improved performance by 25% for certain scenario tests.
54
- // Probably because there was no need to look up the prototype chain anymore.
55
- this.$adaptExpectedToken = null;
56
- this.$adaptExpectedExcludes = [ ];
57
- this.$nextTokensToken = null;
58
- this.$nextTokensContext = null;
59
-
60
- this.options = {};
61
-
62
- this.genericFunctionsStack = [];
63
- this.$genericKeywords = specialFunctions[''][1];
64
- }
65
- }
66
-
67
- // TODO: Use actual methods.
68
- Object.assign(GenericAntlrParser.prototype, {
69
- message(...args) {
70
- return _message( this, 'message', ...args );
71
- },
72
- error(...args) {
73
- return _message( this, 'error', ...args );
74
- },
75
- warning(...args) {
76
- return _message( this, 'warning', ...args );
77
- },
78
- info(...args) {
79
- return _message( this, 'info', ...args );
80
- },
81
- isBetaEnabled,
82
- attachLocation,
83
- assignAnnotation,
84
- addAnnotation,
85
- expressionAsAnnotationValue,
86
- checkExtensionDict,
87
- handleDuplicateExtension,
88
- startLocation,
89
- tokenLocation,
90
- isMultiLineToken,
91
- fixMultiLineTokenEndLocation,
92
- valueWithTokenLocation,
93
- previousTokenAtLocation,
94
- combinedLocation,
95
- surroundByParens,
96
- tokensToStringRepresentation,
97
- secureParens,
98
- unaryOpForParens,
99
- leftAssocBinaryOp,
100
- classifyImplicitName,
101
- warnIfColonFollows,
102
- fragileAlias,
103
- identAst,
104
- reportPathNamedManyOrOne,
105
- reportVirtualAsRef,
106
- reportMissingSemicolon,
107
- pushXprToken,
108
- pushOpToken,
109
- argsExpression,
110
- valuePathAst,
111
- fixNewKeywordPlacement,
112
- signedExpression,
113
- numberLiteral,
114
- unsignedIntegerLiteral,
115
- assignAnnotationValue,
116
- quotedLiteral,
117
- pathName,
118
- docComment,
119
- addDef,
120
- addItem,
121
- addExtension,
122
- createSource,
123
- createDict,
124
- createArray,
125
- finalizeDictOrArray,
126
- insertSemicolon,
127
- setMaxCardinality,
128
- setNullability,
129
- reportDuplicateClause,
130
- reportUnexpectedExtension,
131
- reportUnexpectedSpace,
132
- pushIdent,
133
- pushItem,
134
- handleComposition,
135
- associationInSelectItem,
136
- reportExpandInline,
137
- checkTypeFacet,
138
- checkTypeArgs,
139
- csnParseOnly,
140
- markAsSkippedUntilEOF,
141
- noAssignmentInSameLine,
142
- noSemicolonHere,
143
- setLocalToken,
144
- setLocalTokenIfBefore,
145
- setLocalTokenForId,
146
- excludeExpected,
147
- isStraightBefore,
148
- meltKeywordToIdentifier,
149
- prepareGenericKeywords,
150
- reportErrorForGenericKeyword,
151
- parseMultiLineStringLiteral,
152
- XsnArtifact,
153
- XsnName,
154
- });
155
-
156
- // Use the following function for language constructs which we (currently)
157
- // just being able to parse, in able to run tests from HANA CDS. As soon as we
158
- // create ASTs for the language construct and put it into a CSN, a
159
- // corresponding check should actually be inside the compiler, because the same
160
- // language construct can come from a CSN as source.
161
- // TODO: this is not completely done this way
162
-
163
- // Use the following function for language constructs which we (currently) do
164
- // not really compile, just use to produce a CSN for functions parse.cql() and
165
- // parse.expr().
166
- // This function has a similar interface to our message functions on purpose!
167
- // (tokens ~= location)
168
- function csnParseOnly( msgId, tokens, textArgs ) {
169
- if (!msgId || this.options.parseOnly)
170
- return;
171
- const loc = this.tokenLocation( tokens[0], tokens[tokens.length - 1] );
172
- this.error( msgId, loc, textArgs );
173
- }
174
-
175
- /**
176
- * Do not propose a `;` or closing brace `}` at this position.
177
- *
178
- * Attention: May conflict with excludeExpected()!
179
- *
180
- * @this {object}
181
- * */
182
- function noSemicolonHere() {
183
- const handler = this._errHandler;
184
- const t = this.getCurrentToken();
185
- this.$adaptExpectedToken = t;
186
- this.$adaptExpectedExcludes = [ "';'", "'}'" ];
187
- this.$nextTokensToken = t;
188
- this.$nextTokensContext = null; // match() of WITH does not reset
189
- this.$nextTokensState = ATNState.INVALID_STATE_NUMBER;
190
- if (t.text === ';' && handler && handler.reportIgnoredWith )
191
- handler.reportIgnoredWith( this, t );
192
- }
193
-
194
- /**
195
- * Using this function "during ATN decision making" has no effect
196
- * In front of an ATN decision, you might specify dedicated excludes
197
- * for non-LA1 tokens via a sub-array in excludes[0].
198
- * TODO: consider $nextTokens…, see commented use in rule `elementProperties`
199
- *
200
- * Usage Note:
201
- * Must be used at all positions where sync() is called in the generated coding.
202
- * ```antlr4
203
- * { this.excludeExpected(['ACTIONS']); }
204
- * ( WITH { this.excludeExpected(['ACTIONS']); } )?
205
- * annotationAssignment_ll1[ $art ]* { this.excludeExpected(['ACTIONS']); }
206
- * ACTIONS
207
- * ```
208
- */
209
- function excludeExpected( excludes ) {
210
- if (excludes) {
211
- // @ts-ignore
212
- const t = this.getCurrentToken();
213
- this.$adaptExpectedToken = t;
214
- this.$adaptExpectedExcludes = Array.isArray(excludes) ? excludes : [ excludes ];
215
- this.$nextTokensToken = t;
216
- this.$nextTokensContext = null;
217
- }
218
- }
219
-
220
- function setLocalToken( string, tokenName, notBefore, inSameLine ) {
221
- const ll1 = this.getCurrentToken();
222
- if (ll1.text.toUpperCase() === string &&
223
- (!inSameLine || this._input.LT(-1).line === ll1.line) &&
224
- (!notBefore || !notBefore.test( this._input.LT(2).text )))
225
- ll1.type = this.constructor[tokenName];
226
- }
227
-
228
- function setLocalTokenIfBefore( string, tokenName, before, inSameLine ) {
229
- const ll1 = this.getCurrentToken();
230
- if (ll1.text.toUpperCase() === string &&
231
- (!inSameLine || this._input.LT(-1).line === ll1.line) &&
232
- (!before || before && before.test( this._input.LT(2).text )))
233
- ll1.type = this.constructor[tokenName];
234
- }
235
-
236
- function setLocalTokenForId( offset, tokenNameMap ) {
237
- const tokenName = tokenNameMap[this._input.LT( offset ).text.toUpperCase() || ''];
238
- const ll1 = this.getCurrentToken();
239
- if (tokenName &&
240
- (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )))
241
- ll1.type = this.constructor[tokenName];
242
- return !!tokenName;
243
- }
244
-
245
- // // Special function for rule `requiredSemi` before return $ctx
246
- // function braceForSemi() {
247
- // if (RBRACE == null)
248
- // RBRACE = this.literalNames.indexOf( "'}'" );
249
- // console.log(RBRACE)
250
- // // we are called before match('}') and this.state = ...
251
- // let atn = this._interp.atn;
252
- // console.log( atn.nextTokens( atn.states[ this.state ], this._ctx ) )
253
- // let next = atn.states[ this.state ].transitions[0].target;
254
- // // if a '}' is not possible in the grammar after the fake-'}', throw error
255
- // if (!atn.nextTokens( next, this._ctx ).contains(RBRACE))
256
- // console.log( atn.nextTokens( next, this._ctx ) )
257
- // // throw new antlr4.error.InputMismatchException(this);
258
- // }
259
-
260
- function markAsSkippedUntilEOF() {
261
- let t = this.getCurrentToken();
262
- if (t.type === antlr4.Token.EOF)
263
- return;
264
- if (!t.$isSkipped && !this._errHandler.inErrorRecoveryMode( this )) {
265
- // If not already done, we should report an error if we do not see EOF. We cannot
266
- // use match() here, because these would consume tokens without marking them.
267
- this._errHandler.reportUnwantedToken( this, [ '<EOF>' ] );
268
- t.$isSkipped = 'offending';
269
- this.consume();
270
- t = this.getCurrentToken();
271
- }
272
- while (t.type !== antlr4.Token.EOF) {
273
- t.$isSkipped = true;
274
- this.consume();
275
- t = this.getCurrentToken();
276
- }
277
- }
278
-
279
- function noAssignmentInSameLine() {
280
- const t = this.getCurrentToken();
281
- if (t.text === '@' && t.line <= this._input.LT(-1).line) {
282
- // TODO: use 'syntax-missing-newline'
283
- this.warning( 'syntax-missing-semicolon', t, { code: ';' },
284
- // eslint-disable-next-line @stylistic/js/max-len
285
- 'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' );
286
- }
287
- }
288
-
289
- // Use after matching ',' to allow ',' in front of the closing paren. Be sure
290
- // that you know what to do if successful - break/return/... = check the
291
- // generated grammar; inside loops, you can use `break`. This function is
292
- // still the preferred way to express an optional ',' at the end, because it
293
- // does not influence the error reporting. It might also allow to match
294
- // reserved keywords, because there is no ANTLR generated decision in front of it.
295
- function isStraightBefore( closing ) {
296
- return this.getCurrentToken().text === closing;
297
- }
298
-
299
- function meltKeywordToIdentifier( exceptTrueFalseNull = false ) {
300
- const { Identifier } = this.constructor;
301
- const token = this.getCurrentToken() || { type: Identifier };
302
- if (token.type < Identifier && /^[a-z]+$/i.test( token.text ) &&
303
- !(exceptTrueFalseNull && /^(true|false|null)$/i.test( token.text )))
304
- token.type = Identifier;
305
- }
306
-
307
- const genericTokenTypes = {
308
- expr: 'GenericExpr',
309
- separator: 'GenericSeparator',
310
- intro: 'GenericIntro',
311
- };
312
-
313
- /**
314
- * @memberOf GenericAntlrParser
315
- *
316
- * @param pathItem
317
- * @param [expected]
318
- */
319
- function prepareGenericKeywords( pathItem, expected = null ) {
320
- const length = pathItem?.args?.length || 0;
321
- const argPos = length;
322
- const func = pathItem?.id && specialFunctions[pathItem.id.toUpperCase()];
323
- const spec = func && func[argPos] || specialFunctions[''][argPos ? 1 : 0];
324
- this.$genericKeywords = spec;
325
- // @ts-ignore
326
- const token = this.getCurrentToken() || { text: '' };
327
- const text = token.text.toUpperCase();
328
- let generic = spec[text];
329
- // console.log('PGK:',token.text,generic,expected,spec,func,argPos)
330
- if (expected) { // 'separator' or 'expr' (after 'separator')
331
- if (generic !== expected)
332
- return;
333
- }
334
- else if (!generic || generic === 'separator') {
335
- // Mismatch at beginning (or just an expression): keep token type
336
- // (if not expression, issue error and consider the token to be an
337
- // expression replacement, like ALL)
338
- return;
339
- }
340
- else if (generic === 'expr' && spec.intro && spec.intro.includes( text )) {
341
- // token is both an intro and an expression, like LEADING for TRIM
342
- const next = this._input.LT(2).text;
343
- if (!next || // followed by EOF -> consider it to be 'intro', better for CC
344
- next !== ',' && next !== ')' && spec[next.toUpperCase()] !== 'separator')
345
- generic = 'intro'; // is intro if next token is not separator, not ',', ')'
346
- }
347
- // @ts-ignore
348
- token.type = this.constructor[genericTokenTypes[generic]];
349
- }
350
- // To be called before having matched ( HideAlternatives | … )
351
- function reportErrorForGenericKeyword() {
352
- this._errHandler.reportUnwantedToken( this );
353
- // this._errHandler.reportInputMismatch( this, { offending: this._input.LT(1) }, null );
354
- }
355
-
356
- // Attach location matched by current rule to node `art`. If a location is
357
- // already provided, only set the end location. Use this function only
358
- // in @after actions of parser rules, as the end position is only available
359
- // there.
360
- function attachLocation( art ) {
361
- if (!art || art.$parens)
362
- return art;
363
- if (!art.location) {
364
- art.location = this.tokenLocation(this._ctx.start, this._ctx.stop);
365
- return art;
366
- }
367
- if (!this._ctx.stop)
368
- return art;
369
-
370
- // The last token (this._ctx.stop) may be a multi-line string literal, in which
371
- // case we can't rely on `this._ctx.stop.line`.
372
- if (this.isMultiLineToken(this._ctx.stop)) {
373
- this.fixMultiLineTokenEndLocation(this._ctx.stop, art.location);
374
- }
375
- else {
376
- const { stop } = this._ctx;
377
- art.location.endLine = stop.line;
378
- // after the last char (special for EOF?)
379
- art.location.endCol = stop.stop - stop.start + stop.column + 2;
380
- }
381
-
382
- return art;
383
- }
384
-
385
- function assignAnnotation( art, anno, prefix = '' ) {
386
- const { name, $flatten } = anno;
387
- const { path } = name;
388
- if (path.broken || !path[path.length - 1].id)
389
- return;
390
- const pathname = pathName( path );
391
- let absolute = '';
392
- if (name.variant) {
393
- const variant = pathName( name.variant.path );
394
- absolute = `${ prefix }${ pathname }#${ variant }`;
395
- // We do not care anymore whether we get a second '#' with flattening. This
396
- // can be produced via CSN and with delimited ids anyway. If backends care,
397
- // they need to have their own check.
398
- }
399
- else if (!prefix || pathname !== '$value') {
400
- absolute = `${ prefix }${ pathname }`;
401
- }
402
- else {
403
- absolute = prefix.slice( 0, -1 ); // remove final dot
404
- }
405
-
406
- if ($flatten) {
407
- for (const a of $flatten)
408
- this.assignAnnotation( art, a, `${ absolute }.` );
409
- }
410
- else {
411
- name.id = absolute;
412
- this.addAnnotation( art, `@${ absolute }`, anno );
413
- }
414
- if (!prefix) { // set deprecated $annotations for cds-lsp
415
- if (!art.$annotations)
416
- art.$annotations = [];
417
- const location = locUtils.combinedLocation( anno.name, anno );
418
- art.$annotations.push( { value: anno, location } );
419
- }
420
- }
421
-
422
- function addAnnotation( art, prop, anno ) {
423
- const old = art[prop];
424
- if (old) {
425
- this.error( 'syntax-duplicate-anno', old.name.location, { anno: prop },
426
- 'Assignment for $(ANNO) is overwritten by another one below' );
427
- }
428
- art[prop] = anno;
429
- }
430
-
431
- const extensionDicts = {
432
- elements: true, enum: true, params: true, returns: true,
433
- };
434
-
435
- function checkExtensionDict( dict ) {
436
- for (const name in dict) {
437
- const def = dict[name];
438
- if (!def.$duplicates)
439
- continue;
440
-
441
- if (def.kind !== 'annotate') {
442
- const numDefines
443
- = def.$duplicates.reduce( addOneForDefinition, addOneForDefinition( 0, def ) );
444
- this.handleDuplicateExtension( def, name, numDefines );
445
- for (const dup of def.$duplicates)
446
- this.handleDuplicateExtension( dup, name, numDefines );
447
- continue;
448
- }
449
- // move annotations, 'doc' and 'elements' etc to main member
450
- for (const dup of def.$duplicates) {
451
- for (const prop of Object.keys( dup )) {
452
- if (prop.charAt(0) === '@') {
453
- this.addAnnotation( def, prop, dup[prop] );
454
- delete dup[prop]; // we want to keep $duplicates, but not have duplicate props
455
- }
456
- else if (prop === 'doc') {
457
- // With explicit docComment:false, we don't emit a warning.
458
- if (def.doc && this.options.docComment !== false) {
459
- this.warning( 'syntax-duplicate-doc-comment', def.doc.location, {},
460
- 'Doc comment is overwritten by another one below' );
461
- }
462
- def.doc = dup.doc;
463
- delete dup[prop]; // we want to keep $duplicates for LSP, but not have duplicate props
464
- }
465
- else if (extensionDicts[prop]) {
466
- if (def[prop])
467
- this.message( 'syntax-duplicate-annotate', [ def.name.location ], { name, prop } );
468
- def[prop] = dup[prop]; // continuation semantics: last wins
469
- delete dup[prop]; // we want to keep $duplicates for LSP, but not have duplicate props
470
- }
471
- }
472
- if (dup.$annotations) { // update deprecated $annotations for cds-lsp / annotation modeler
473
- if (def.$annotations)
474
- def.$annotations.push( ...dup.$annotations );
475
- else
476
- def.$annotations = dup.$annotations;
477
- }
478
- }
479
-
480
- // We keep duplicate statements for LSP, as it needs to traverse all identifiers;
481
- // annotations were removed above to avoid traversing annotations twice.
482
- }
483
- }
484
-
485
- function addOneForDefinition( count, ext ) {
486
- return (ext.kind === 'extend') ? count : count + 1;
487
- }
488
-
489
- /**
490
- * Handle duplicate extensions. Does not handle `annotate`.
491
- *
492
- * @param {XSN.Extension} ext
493
- * @param {string} name
494
- * @param {number} numDefines
495
- */
496
- function handleDuplicateExtension( ext, name, numDefines ) {
497
- if (ext.kind === 'extend') {
498
- this.error( 'syntax-duplicate-extend', [ ext.name.location ],
499
- { name, '#': (numDefines ? 'define' : 'extend') } );
500
- }
501
- else if (numDefines === 1) {
502
- ext.$errorReported = 'syntax-duplicate-extend';
503
- } // a definition, but not duplicate
504
- }
505
-
506
-
507
- /**
508
- * Return start location of `token`, or the first token matched by the current
509
- * rule if `token` is undefined
510
- *
511
- * @returns {Location}
512
- */
513
- function startLocation( token = this._ctx.start ) {
514
- return new Location(
515
- this.filename,
516
- token.line,
517
- token.column + 1
518
- );
519
- }
520
-
521
- /**
522
- * Return location of `token`. If `endToken` is provided, use its end
523
- * location as end location in the result.
524
- *
525
- * @param {object} token
526
- * @param {object} endToken
527
- * @return {Location}
528
- */
529
- function tokenLocation( token, endToken = null ) {
530
- if (!token)
531
- return undefined;
532
- if (!endToken) // including null
533
- endToken = token;
534
-
535
- // Default for single line tokens
536
- const endLine = endToken.line;
537
- // after the last char (special for EOF?)
538
- const endCol = endToken.stop - endToken.start + endToken.column + 2;
539
- const loc = new Location( this.filename, token.line, token.column + 1, endLine, endCol );
540
-
541
- // This check is done for performance reason. No need to access a token's
542
- // data if we know that it spans only one single line.
543
- if (this.isMultiLineToken(token))
544
- this.fixMultiLineTokenEndLocation(token, loc);
545
-
546
- return loc;
547
- }
548
-
549
- function isMultiLineToken( token ) {
550
- return (
551
- token.type === this.constructor.DocComment ||
552
- token.type === this.constructor.String || // TODO: do not check every string content
553
- token.type === this.constructor.UnterminatedLiteral
554
- );
555
- }
556
-
557
- /**
558
- * Adapt end location of `location` according to `token`, assuming that `token` is a multi-line
559
- * token such as a multi-line string or doc comment.
560
- *
561
- * Sets `endLine`/`endCol`, respecting newline characters in the token.
562
- *
563
- * @param token
564
- * @param {CSN.Location} location
565
- */
566
- function fixMultiLineTokenEndLocation( token, location ) {
567
- // Count the number of newlines in the token.
568
- const source = token.source[1].data;
569
- let newLineCount = 0;
570
- let lastNewlineIndex = token.start;
571
- for (let i = token.start; i < token.stop; i++) {
572
- // Note: We do NOT check for CR, LS, and PS (/[\r\u2028\u2029]/)
573
- // because ANTLR only uses LF for line break detection.
574
- if (source[i] === 10) { // code point of '\n'
575
- newLineCount++;
576
- lastNewlineIndex = i;
577
- }
578
- }
579
- if (newLineCount > 0) {
580
- location.endLine = token.line + newLineCount;
581
- location.endCol = token.stop - lastNewlineIndex + 1;
582
- }
583
- else {
584
- location.endLine = token.line;
585
- // after the last char (special for EOF?)
586
- location.endCol = token.stop - token.start + token.column + 2;
587
- }
588
- }
589
-
590
- /**
591
- * Return `val` with a location; if `val` and `endToken` are not provided, use the
592
- * lower-cased token string of `startToken` as `val`. As location, use the
593
- * location covered by `startToken` and `endToken`, or only `startToken` if no
594
- * `endToken` is provided. The `startToken` defaults to the previous token.
595
- *
596
- * @param {object} startToken
597
- * @param {object} endToken
598
- * @param {any} val
599
- */
600
- function valueWithTokenLocation( val = undefined, startToken = this._input.LT(-1),
601
- endToken = undefined ) {
602
- // if (!startToken)
603
- // startToken = this._input.LT(-1);
604
- const loc = this.tokenLocation( startToken, endToken );
605
- return {
606
- location: loc,
607
- val: (endToken || val !== undefined) ? val : startToken.text.toLowerCase(),
608
- };
609
- }
610
-
611
- function previousTokenAtLocation( location ) {
612
- let k = -1;
613
- let token = this._input.LT(k);
614
- while (token.line > location.line ||
615
- token.line === location.line && token.column >= location.col)
616
- token = this._input.LT(--k);
617
- return (token.line === location.line && token.column + 1 === location.col) && token;
618
- }
619
-
620
- // Create a location with location properties `filename` and `start` from
621
- // argument `start`, and location property `end` from argument `end`.
622
- function combinedLocation( start, end ) {
623
- if (!start || !start.location)
624
- start = { location: this.startLocation() };
625
- return locUtils.combinedLocation( start, end );
626
- }
627
-
628
- // make sure that the parens of `IN (…)` do not disappear:
629
- function secureParens( expr ) {
630
- const op = expr?.op?.val;
631
- const $parens = expr?.$parens;
632
- if (!$parens || expr.query || op && op !== 'call' && op !== 'cast')
633
- return expr;
634
- // ensure that references, literals and functions keep their surrounding parentheses
635
- // (is for expressions the case anyway)
636
- delete expr.$parens;
637
- return {
638
- op: { val: 'xpr', location: this.startLocation() },
639
- args: [ expr ],
640
- location: { __proto__: Location.prototype, ...expr.location },
641
- $parens,
642
- };
643
- }
644
-
645
- function surroundByParens( expr, open, close, asQuery = false ) {
646
- if (!expr)
647
- return expr;
648
- const location = this.tokenLocation( open, close );
649
- if (expr.$parens)
650
- expr.$parens.push( location );
651
- else
652
- expr.$parens = [ location ];
653
- if (expr.$opPrecedence)
654
- expr.$opPrecedence = null;
655
- return (asQuery) ? { query: expr, location } : expr;
656
- }
657
-
658
-
659
- function tokensToStringRepresentation( start, stop ) {
660
- const tokens = this._input.getTokens(
661
- start.tokenIndex,
662
- stop.tokenIndex + 1, null
663
- ).filter(tok => tok.channel === antlr4.Token.DEFAULT_CHANNEL);
664
- if (tokens.length === 0)
665
- return '';
666
-
667
- let result = tokens[0].text;
668
- for (let i = 1; i < tokens.length; ++i) {
669
- const str = normalizeNewLine(tokens[i].text);
670
- result += (tokens[i].start > tokens[i - 1].stop + 1) ? ` ${ str }` : str;
671
- }
672
- return result;
673
- }
674
-
675
- function unaryOpForParens( query, val ) {
676
- const parens = query?.$parens;
677
- if (!parens)
678
- return query;
679
- const location = parens[parens.length - 1];
680
- return { op: { val, location }, location, args: [ query ] };
681
- }
682
-
683
- // ANTLR on some OS might corrupt non-ASCII chars for messages
684
- function warnIfColonFollows( anno ) {
685
- const t = this.getCurrentToken();
686
- if (t.text === ':') {
687
- this.warning( 'syntax-missing-parens', anno.name.location,
688
- { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
689
- // eslint-disable-next-line @stylistic/js/max-len
690
- 'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
691
- }
692
- }
693
-
694
- // If the token before the current one is a doc comment (ignoring other tokens
695
- // on the hidden channel), put its "cleaned-up" text as value of property `doc`
696
- // of arg `node` (which could be an array). Complain if `doc` is already set.
697
- //
698
- // The doc comment token is not a non-hidden token for the following reasons:
699
- // - misplaced doc comments would lead to a parse error (incompatible),
700
- // - would influence the prediction, probably even induce adaptivePredict() calls,
701
- // - is only slightly "more declarative" in the grammar.
702
- function docComment( node ) {
703
- const token = this._input.getHiddenTokenToLeft( this.constructor.DocComment );
704
- if (!token)
705
- return;
706
-
707
- // This token is actually used by / assigned to an artifact.
708
- token.isUsed = true;
709
-
710
- // With explicit docComment:false, we don't emit a warning.
711
- if (node.doc && this.options.docComment !== false) {
712
- this.warning( 'syntax-duplicate-doc-comment', node.doc.location, {},
713
- 'Doc comment is overwritten by another one below' );
714
- }
715
-
716
- // Either store the doc comment or a marker that there is one.
717
- const val = !this.options.docComment ? true : parseDocComment( token.text );
718
- node.doc = this.valueWithTokenLocation( val, token );
719
- }
720
-
721
- /**
722
- * Classify token (identifier category) for implicit names. To be used in the
723
- * empty alternative to AS <explicitName>. If `ref` is given, uses the last
724
- * path segment's `tokenIndex`. The return value can be used to reset the
725
- * token's category, e.g. for inline select items.
726
- *
727
- * @param {string} category
728
- * @param [ref]
729
- */
730
- function classifyImplicitName( category, ref ) {
731
- if (!ref || ref.path) {
732
- const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
733
- const implicit = (tokenIndex === undefined) ? this._input.LT(-1) : this._input.get(tokenIndex);
734
- if (implicit.isIdentifier) {
735
- const previous = implicit.isIdentifier;
736
- implicit.isIdentifier = category;
737
- return { token: implicit, previous };
738
- }
739
- }
740
- return null;
741
- }
742
-
743
- function fragileAlias( ast, safe = false ) {
744
- if (this.getCurrentToken().text === '.')
745
- return ast;
746
- if (safe || ast.$delimited || !/^[a-zA-Z][a-zA-Z_]+$/.test( ast.id )) {
747
- this.warning( 'syntax-deprecated-auto-as', ast.location, { keyword: 'as' },
748
- 'Add keyword $(KEYWORD) in front of the alias name' );
749
- }
750
- else { // configurable error
751
- this.message( 'syntax-missing-as', ast.location, { keyword: 'as' },
752
- 'Add keyword $(KEYWORD) in front of the alias name' );
753
- }
754
- return ast;
755
- }
756
-
757
- // Return AST for identifier token `token`. Also check that identifier is not empty.
758
- function identAst( token, category, noTokenTypeCheck = false ) {
759
- if (!token) { // for rule identAst
760
- const { start, stop } = this._ctx; // token.tokenIndex
761
- // - correct parsing: start = stop
762
- // - singleTokenDeletion(), e.g. with `| Ident`: start < stop → stop
763
- // - after recoverInline: start > stop (!) → stop = the previous token, if it is
764
- // ident-like and the one before not in `.@#`, → start ('') otherwise
765
- token = stop;
766
- if (start.tokenIndex > stop.tokenIndex &&
767
- (stop.type !== this.constructor.Identifier && !/^[a-zA-Z_]+$/.test( stop.text ) ||
768
- [ '.', '@', '#' ].includes( this._input.LT(-2)?.text )))
769
- token = start;
770
- }
771
- token.isIdentifier = category;
772
- let id = token.text;
773
- if (!noTokenTypeCheck &&
774
- token.type !== this.constructor.Identifier && !/^[a-zA-Z_]+$/.test( id ))
775
- id = '';
776
- if (token.text[0] === '!') {
777
- id = id.slice( 2, -1 ).replace( /]]/g, ']' );
778
- if (!id)
779
- this.message( 'syntax-invalid-name', token, {} );
780
-
781
- // $delimited is used to complain about ![$self] and other magic vars usage;
782
- // we might complain about that already here via @arg{category}
783
-
784
- const ast = { id, $delimited: true, location: this.tokenLocation( token ) };
785
- ast.location.tokenIndex = token.tokenIndex;
786
- return ast;
787
- }
788
- if (token.text[0] !== '"') {
789
- const ast = { id, location: this.tokenLocation(token) };
790
- ast.location.tokenIndex = token.tokenIndex;
791
- return ast;
792
- }
793
- // delimited:
794
- id = id.slice( 1, -1 ).replace( /""/g, '"' );
795
- if (!id) {
796
- this.message( 'syntax-invalid-name', token, {} );
797
- }
798
- else {
799
- this.message( 'syntax-deprecated-ident', token, { delimited: id },
800
- // eslint-disable-next-line @stylistic/js/max-len
801
- 'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' );
802
- }
803
- const ast = { id, $delimited: true, location: this.tokenLocation( token ) };
804
- ast.location.tokenIndex = token.tokenIndex;
805
- return ast;
806
- }
807
-
808
- function reportPathNamedManyOrOne( { path } ) {
809
- if (path.length === 1 && !path[0].$delimited &&
810
- [ 'many', 'one' ].includes( path[0].id.toLowerCase() )) {
811
- this.message( 'syntax-unexpected-many-one', path[0].location,
812
- { code: path[0].id, delimited: path[0].id } );
813
- }
814
- }
815
-
816
- function reportVirtualAsRef() {
817
- const { type, text } = this._input.LT(2);
818
- if (this.constructor.Number < type && type <= this.constructor.Identifier ||
819
- [ '+', '-', '(' ].includes( text )) {
820
- // remark: we do not need to include 'not', as condition operators are only
821
- // allowed inside parentheses in the old parser
822
- const token = this._input.LT(1);
823
- this.message( 'syntax-deprecated-ref-virtual', token, {
824
- '#': (text === '(' ? 'func' : 'ref'),
825
- name: token.text,
826
- delimited: token.text,
827
- } );
828
- }
829
- }
830
-
831
- function reportMissingSemicolon() {
832
- const next = this._input.LT(1);
833
- if (next.text !== ';' && next.text !== '' && // ';' by insertSemicolon()
834
- next.text !== '}' && next.type !== antlr4.Token.EOF &&
835
- this._input.LT(-1).text !== '}') {
836
- const offending = this.literalNames[next.type] || this.symbolicNames[next.type];
837
- const loc = this.tokenLocation( this._input.LT(-1) );
838
- // better location after the previous token:
839
- const location = new Location( loc.file, loc.endLine, loc.endCol );
840
- // it would be nicer to mention the doc comment if present, but not worth the
841
- // effort; 'syntax-missing-semicolon' already used
842
- this.warning( 'syntax-missing-proj-semicolon', location,
843
- { expecting: [ "';'" ], offending },
844
- 'Missing $(EXPECTING) before $(OFFENDING)');
845
- }
846
- }
847
-
848
- function pushXprToken( args ) {
849
- const token = this._input.LT(-1);
850
- args.push( {
851
- location: this.tokenLocation( token ),
852
- val: token.text.toLowerCase(), // TODO: remove toLowerCase() ?
853
- literal: 'token',
854
- } );
855
- }
856
-
857
- function valuePathAst( ref ) {
858
- // TODO: XSN representation of functions is a bit strange - rework
859
- const { path } = ref;
860
- if (!path || path.broken)
861
- return ref;
862
- if (path.length === 1) {
863
- const { args, id, location } = path[0];
864
- if (args
865
- ? path[0].$syntax === ':'
866
- : path[0].$delimited || !functionsWithoutParentheses.includes( id.toUpperCase() ))
867
- return ref;
868
-
869
- const implicit = this.previousTokenAtLocation( location );
870
- if (implicit && implicit.isIdentifier)
871
- implicit.isIdentifier = 'func';
872
-
873
- const filter = path[0].cardinality || path[0].where;
874
- if (filter)
875
- this.message( 'syntax-unexpected-filter', filter.location, {} );
876
- const op = { location, val: 'call' };
877
- return (args)
878
- ? {
879
- op, func: ref, location: ref.location, args,
880
- }
881
- : { op, func: ref, location: ref.location };
882
- }
883
-
884
-
885
- // $syntax === ':' => path(P: 1)
886
- // $syntax !== ':' => path(P => 1) or path(1) or path()
887
- const firstFunc = path.findIndex( i => i.args && i.$syntax !== ':' );
888
- if (firstFunc === -1) // also covers empty paths
889
- return ref;
890
-
891
-
892
- // Method Call ---------------------------
893
- // Transform the path into `.`-operators.
894
- // Everything after the first function is also a function, and not a reference.
895
-
896
- for (let i = firstFunc; i < path.length; ++i) {
897
- if (path[i].args && path[i].$syntax === ':') {
898
- // Error for `a(P => 1).b.c(P: 1)`: no ref after function.
899
- this.$messageFunctions.error('syntax-invalid-ref', path[i].args[$location], {
900
- code: '=>',
901
- }, 'References after function calls can\'t be resolved. Use $(CODE) in function arguments');
902
- break;
903
- }
904
- const filter = path[i].cardinality || path[i].where;
905
- if (filter)
906
- this.message( 'syntax-unexpected-filter', filter.location, {} );
907
- }
908
-
909
- const args = [];
910
- if (firstFunc > 0) {
911
- args.push({
912
- path: path.slice(0, firstFunc),
913
- location: locUtils.combinedLocation(path[0].location, path[path.length - 1].location),
914
- });
915
- }
916
-
917
- const pathRest = path.slice(firstFunc);
918
- for (const method of pathRest) {
919
- if (method !== pathRest[0] || firstFunc > 0) {
920
- args.push({
921
- // TODO: Update parser to have proper location for `.`?
922
- location: weakLocation(method.location),
923
- val: '.',
924
- literal: 'token',
925
- });
926
- }
927
- const func = {
928
- op: { location: method.location, val: 'call' },
929
- func: { path: [ method ] },
930
- location: method.location,
931
- };
932
- if (method.args)
933
- func.args = method.args;
934
- args.push(func);
935
- }
936
-
937
- return {
938
- op: {
939
- val: 'ixpr',
940
- location: this.startLocation(),
941
- },
942
- args,
943
- location: ref.location,
944
- };
945
- }
946
-
947
-
948
- /**
949
- * Adds the first argument of `args` ('new' keyword) to the second argument, if it's a method-ixpr.
950
- *
951
- * @todo Cleanup, remove.
952
- * @param args
953
- */
954
- function fixNewKeywordPlacement( args ) {
955
- // TODO: Currently, the parser creates an args-array with `new` and an `ixpr` for
956
- // `new P().abc()`. That is, "new" is separate from the methods.
957
- // This function tries to work around it, but its more of a hack.
958
- if (args.length !== 2 || !args[1].args || args[1].op?.val !== 'ixpr')
959
- return;
960
- const ixpr = args[1];
961
- ixpr.args.unshift(args[0]);
962
- args.length = 0;
963
- args.push(ixpr);
964
- }
965
-
966
- function expressionAsAnnotationValue( assignment, cond, start, stop ) {
967
- if (!cond) // parse error
968
- return;
969
- Object.assign(assignment, cond);
970
- assignment.$tokenTexts = this.tokensToStringRepresentation( start, stop );
971
- }
972
-
973
- // If a '-' is directly before an unsigned number, consider it part of the number;
974
- // otherwise (including for '+'), represent it as extra unary prefix operator.
975
- function signedExpression( args, expr ) {
976
- // if (args.length !== 1) throw new CompilerAssertion()
977
- const sign = args[0];
978
- const nval
979
- = (sign.val === '-' &&
980
- expr && // expr may be null if `-` rule can't be parsed
981
- expr.literal === 'number' &&
982
- sign.location.endLine === expr.location.line &&
983
- sign.location.endCol === expr.location.col &&
984
- (typeof expr.val === 'number'
985
- ? expr.val >= 0 && -expr.val
986
- : !expr.val.startsWith('-') && `-${ expr.val }`)) || false;
987
- if (nval === false) {
988
- args.push( expr );
989
- }
990
- else {
991
- expr.val = nval;
992
- --expr.location.col;
993
- args[0] = expr;
994
- }
995
- }
996
-
997
- /**
998
- * Return number literal (XSN) for number token `token` with optional token `sign`.
999
- * Represent the number as a JS number in property `val` if the number can safely be
1000
- * represented as one. Represent the number by a string, the token lexeme, if the
1001
- * stringified version of the number does not match the token lexeme.
1002
- *
1003
- * TODO: Always use text !== `${ num }`
1004
- */
1005
- function numberLiteral( sign, text = this._input.LT(-1).text ) {
1006
- const token = this._input.LT(-1);
1007
- let location = this.tokenLocation( token );
1008
- const nextToken = this._input.LT(1);
1009
- if (token.type === this.constructor.Number &&
1010
- token.stop + 1 === nextToken.start &&
1011
- (nextToken.type === this.constructor.Identifier ||
1012
- nextToken.type < this.constructor.Identifier && /^[a-z]+$/i.test( nextToken.text ))) {
1013
- this.message('syntax-expecting-space', nextToken, {},
1014
- 'Expecting a space between a number and a keyword/identifier');
1015
- }
1016
-
1017
- if (sign) {
1018
- const { endLine, endCol } = location;
1019
- location = this.startLocation( sign );
1020
- location.endLine = endLine;
1021
- location.endCol = endCol;
1022
- text = sign.text + text;
1023
- this.reportUnexpectedSpace( sign, this.tokenLocation( token ) );
1024
- }
1025
-
1026
- const num = Number.parseFloat( text || '0' ); // not Number.parseInt() !
1027
- const normalized = normalizeNumberString(text);
1028
- if (normalized !== `${ num }` && normalized !== `${ sign.text }${ num }`)
1029
- return { literal: 'number', val: normalized, location };
1030
- return { literal: 'number', val: num, location };
1031
- }
1032
-
1033
- /**
1034
- * Given `token`, return a number literal (XSN). If the number is not an unsigned integer
1035
- * or it can't be represented in JS, emit an error.
1036
- */
1037
- function unsignedIntegerLiteral() {
1038
- const token = this._input.LT(-1);
1039
- const location = this.tokenLocation( token );
1040
- const text = token.text || '0';
1041
- const num = Number.parseFloat( text ); // not Number.parseInt() !
1042
- if (!Number.isSafeInteger(num)) {
1043
- this.error( 'syntax-expecting-unsigned-int', token,
1044
- { '#': !text.match(/^\d*$/) ? 'normal' : 'unsafe' } );
1045
- }
1046
- else if (text.match(/^\d+[.]\d+$/)) {
1047
- // More restrictive check: 10.0 emits a message, because we don't expect
1048
- // any decimal places.
1049
- const dotLoc = { ...location };
1050
- dotLoc.col += text.indexOf('.');
1051
- dotLoc.endCol = dotLoc.col + 1;
1052
- this.info( 'syntax-ignoring-decimal', dotLoc );
1053
- }
1054
- return { literal: 'number', val: num, location };
1055
- }
1056
-
1057
- // Make the annotation `anno` have `value` as value. This function is basically
1058
- // just `Object.assign`, but we really try to represent the provided CDL number as
1059
- // JSON number. We give a warning if this is not possible or leads to a precision
1060
- // loss.
1061
- function assignAnnotationValue( anno, value ) {
1062
- const { val } = value;
1063
- if (value.literal === 'number' && typeof val !== 'number') {
1064
- // a number in CDL, but stored as string in `val` - due to rounding or scientific notation
1065
- let num = Number.parseFloat( val || '0' );
1066
- const inf = !Number.isFinite( num );
1067
- if (inf)
1068
- num = val;
1069
- if (inf || relevantDigits( val ) !== relevantDigits( num.toString() )) {
1070
- this.warning( 'syntax-invalid-anno-number', value.location,
1071
- { '#': (inf ? 'infinite' : 'rounded' ), rawvalue: val, value: num },
1072
- {
1073
- std: 'Annotation number $(RAWVALUE) is put as $(VALUE) into the CSN',
1074
- rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)',
1075
- // eslint-disable-next-line @stylistic/js/max-len
1076
- infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN',
1077
- } );
1078
- }
1079
- value.val = num;
1080
- }
1081
- Object.assign( anno, value );
1082
- }
1083
-
1084
- function relevantDigits( val ) {
1085
- // We know the value does not contain newlines, hence the RegEx is safe.
1086
- // eslint-disable-next-line sonarjs/slow-regex
1087
- val = val.replace( /e.+$/i, '' );
1088
-
1089
- // To avoid the super-linear RegEx `0+$`, use the non-backtracking version and
1090
- // simply check if we're at the end.
1091
- const trailingZeroes = /0+/g;
1092
- let re;
1093
- while ((re = trailingZeroes.exec(val)) !== null) {
1094
- if (trailingZeroes.lastIndex === val.length) {
1095
- val = val.slice(0, re.index);
1096
- break;
1097
- }
1098
- }
1099
-
1100
- return val
1101
- .replace( /\./, '' )
1102
- .replace( /^[-+0]+/, '' );
1103
- }
1104
-
1105
- // Create AST node for quoted literals like string and e.g. date'2017-02-22'.
1106
- // This function might issue a message and might change the `literal` and
1107
- // `val` property according to `quotedLiteralPatterns` above.
1108
- function quotedLiteral( token, literal ) {
1109
- /** @type {CSN.Location} */
1110
- const location = this.tokenLocation( token );
1111
- let pos;
1112
- let val;
1113
-
1114
- if (token.text.startsWith('`')) {
1115
- val = this.parseMultiLineStringLiteral(token);
1116
- literal = 'string';
1117
- }
1118
- else {
1119
- pos = token.text.search( '\'' ) + 1; // pos of char after quote
1120
- val = token.text.slice( pos, -1 ).replace( /''/g, '\'' );
1121
- }
1122
-
1123
- if (!literal)
1124
- literal = token.text.slice( 0, pos - 1 ).toLowerCase();
1125
- const p = quotedLiteralPatterns[literal] || {};
1126
-
1127
- if (p.test_fn && !p.test_fn(val) && !this.options.parseOnly)
1128
- this.warning( 'syntax-invalid-literal', location, { '#': p.test_variant } );
1129
-
1130
- if (p.unexpected_char) {
1131
- const idx = val.search(p.unexpected_char);
1132
- if (idx > -1) {
1133
- this.warning( 'syntax-invalid-literal', {
1134
- file: location.file,
1135
- line: location.line,
1136
- endLine: location.line,
1137
- col: atChar(idx),
1138
- endCol: atChar( idx + (val[idx] === '\'' ? 2 : 1) ),
1139
- }, { '#': p.unexpected_variant } );
1140
- }
1141
- }
1142
- return {
1143
- literal: p.literal || literal,
1144
- val: p.normalize && p.normalize(val) || val,
1145
- location,
1146
- };
1147
-
1148
- function atChar( i ) {
1149
- // Is only used with single-line strings.
1150
- return location.col + pos + i;
1151
- }
1152
- }
1153
-
1154
- function pushIdent( path, ident, prefix ) {
1155
- if (!ident) {
1156
- path.broken = true;
1157
- }
1158
- else if (!prefix) {
1159
- path.push( ident );
1160
- }
1161
- else {
1162
- const { location } = ident;
1163
- const prefixLoc = this.reportUnexpectedSpace( prefix, location );
1164
- location.line = prefixLoc.line;
1165
- location.col = prefixLoc.col;
1166
- ident.id = prefix.text + ident.id;
1167
- path.push( ident );
1168
- }
1169
- }
1170
-
1171
- function pushItem( array, val ) {
1172
- if (!array)
1173
- return;
1174
-
1175
- if (val != null)
1176
- array.push(val);
1177
- else
1178
- array.broken = true;
1179
- }
1180
-
1181
- // For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths
1182
- function reportUnexpectedSpace( prefix = this._input.LT(-1),
1183
- location = this.tokenLocation( this._input.LT(1) ),
1184
- isError = false ) {
1185
- const prefixLoc = this.tokenLocation( prefix );
1186
- if (prefixLoc.endLine !== location.line ||
1187
- prefixLoc.endCol !== location.col) {
1188
- const wsLocation = {
1189
- file: location.file,
1190
- line: prefixLoc.endLine, // !
1191
- col: prefixLoc.endCol, // !
1192
- endLine: location.line,
1193
- endCol: location.col,
1194
- };
1195
- if (isError) {
1196
- this.message( 'syntax-invalid-space', wsLocation, { op: prefix.text },
1197
- 'Delete the whitespace after $(OP)' );
1198
- }
1199
- else {
1200
- this.warning( 'syntax-unexpected-space', wsLocation, { op: prefix.text },
1201
- 'Delete the whitespace after $(OP)' );
1202
- }
1203
- }
1204
- return prefixLoc;
1205
- }
1206
-
1207
- // Add new definition `art` to dictionary property `env` of node `parent`.
1208
- // Return `art`.
1209
- //
1210
- // If argument `kind` is provided, set `art.kind` to that value.
1211
- // If argument `name` is provided, set `art.name`:
1212
- // - if `name` is an array, `name.id` consist of the ID of the last array item
1213
- // (for elements via columns, foreign keys, table aliases)
1214
- // - if `name` is an object, `name.id` is either set, or the (local) name is calculated
1215
- // from the IDs of all items in `name.path` (for main artifact definitions).
1216
- function addDef( art, parent, env, kind, name ) {
1217
- if (Array.isArray(name)) {
1218
- const last = name.length && name[name.length - 1];
1219
- art.name = { // A.B.C -> 'C'
1220
- id: last?.id || '', location: last.location, $inferred: 'as',
1221
- };
1222
- }
1223
- else if (name) {
1224
- art.name = name;
1225
- if (!name.id && kind === null) // namedValue, fortunately no `variant` there
1226
- art.name.id = pathName( art.name?.path );
1227
- }
1228
- else {
1229
- art.name = { id: '' };
1230
- }
1231
- if (kind)
1232
- art.kind = kind;
1233
-
1234
- const id = art.name?.id || pathName( art.name?.path ); // returns '' for corrupted name
1235
-
1236
- if (env === 'artifacts' || env === 'vocabularies') {
1237
- dictAddArray( parent[env], id, art );
1238
- }
1239
- else if (kind || this.options.parseOnly) { // TODO: do not check parseOnly
1240
- dictAdd( parent[env], id, art );
1241
- }
1242
- else {
1243
- dictAdd( parent[env], id, art, ( duplicateName, loc ) => {
1244
- // do not use function(), otherwise `this` is wrong:
1245
- if (kind === 0) {
1246
- this.error( 'syntax-duplicate-argument', loc, { name: duplicateName },
1247
- 'Duplicate value for parameter $(NAME)' );
1248
- }
1249
- else if (kind === '') {
1250
- this.error( 'syntax-duplicate-excluding', loc,
1251
- { name: duplicateName, keyword: 'excluding' } );
1252
- }
1253
- else {
1254
- this.error( 'syntax-duplicate-property', loc, { name: duplicateName },
1255
- 'Duplicate value for structure property $(NAME)' );
1256
- }
1257
- } );
1258
- }
1259
- return art;
1260
- }
1261
-
1262
- // Add new definition `art` to array property `env` of node `parent`.
1263
- // Also set `kind`. Returns `art`.
1264
- function addItem( art, parent, env, kind ) {
1265
- art.kind = kind;
1266
- parent[env].push( art );
1267
- return art;
1268
- }
1269
- /**
1270
- * Add `annotate/extend Main.Artifact:elem.sub` to `‹xsn›.extensions`:
1271
- * - the array item is an extend/annotate for `Main.Artifact`,
1272
- * - for each path item in `elem.sub`, we add an `elements` property containing
1273
- * one extend/annotate for the corresponding element
1274
- * - The deepest extend/annotate is the object which is to be extended
1275
- *
1276
- * @param {object} ext The object containing the location and annotations for the extension.
1277
- * @param {object} parent The parent containing the `extensions` property, i.e. the source.
1278
- * @param {string} kind Either `annotate` or `extend`.
1279
- * @param {object} artName The "name object" for `Main.Artifact`.
1280
- * @param {XSN.Path} elemPath Path as returned by `simplePath` rule.
1281
- */
1282
- function addExtension( ext, parent, kind, artName, elemPath ) {
1283
- const { location } = ext;
1284
- if (!Array.isArray( elemPath ) || !elemPath.length || elemPath.broken) {
1285
- ext.name = artName;
1286
- this.addItem( ext, parent, 'extensions', kind );
1287
- return;
1288
- }
1289
- // Note: the element extensions share a common `location`, also with the
1290
- // extension of the main artifact; its end location will usually set later
1291
- parent = this.addItem( { name: artName, location }, parent, 'extensions', kind );
1292
-
1293
- const last = elemPath[elemPath.length - 1];
1294
- for (const seg of elemPath) {
1295
- parent.elements = Object.create(null); // no dict location → no createDict()
1296
- parent = this.addDef( (seg === last ? ext : { location }),
1297
- parent, 'elements', kind, seg );
1298
- }
1299
- }
1300
-
1301
- // must be in action directly after having parsed '{', '(`, or a keyword before
1302
- function createDict() {
1303
- const dict = Object.create(null);
1304
- dict[$location] = this.startLocation( this._input.LT(-1) );
1305
- return dict;
1306
- }
1307
-
1308
- // must be in action directly after having parsed '[' or '(` or `{`
1309
- function createArray() {
1310
- const array = [];
1311
- array[$location] = this.startLocation( this._input.LT(-1) );
1312
- return array;
1313
- }
1314
-
1315
- // must be in action directly after having parsed '}' or ')`
1316
- function finalizeDictOrArray( dict ) {
1317
- const loc = dict[$location];
1318
- if (!loc)
1319
- return;
1320
- const stop = this._input.LT(-1);
1321
- loc.endLine = stop.line;
1322
- loc.endCol = stop.stop - stop.start + stop.column + 2;
1323
- }
1324
-
1325
- function insertSemicolon() {
1326
- const currentToken = this._input.tokens[this._input.index];
1327
- const requireSemicolon = this.topLevelKeywords.includes(currentToken.type);
1328
-
1329
- if (requireSemicolon) {
1330
- this.noAssignmentInSameLine();
1331
- const prev = this._input.LT(-1);
1332
- const t = CommonTokenFactory.create(
1333
- currentToken.source,
1334
- this.literalNames.indexOf( "';'" ),
1335
- '', antlr4.Token.DEFAULT_CHANNEL,
1336
- prev.stop, prev.stop,
1337
- prev.line, prev.column
1338
- );
1339
-
1340
- t.tokenIndex = prev.tokenIndex + 1;
1341
-
1342
- this._input.tokens.splice(t.tokenIndex, 0, t);
1343
-
1344
- // Update tokenIndex: There could have been comments between two non-hidden tokens.
1345
- for (let tokenIndex = t.tokenIndex + 1; tokenIndex < this._input.tokens.length; tokenIndex++)
1346
- this._input.tokens[tokenIndex].tokenIndex += 1;
1347
-
1348
- this._input.index = t.tokenIndex;
1349
- }
1350
- }
1351
-
1352
- function createSource() {
1353
- return new XsnSource();
1354
- }
1355
-
1356
- const operatorPrecedences = {
1357
- // query:
1358
- union: 1,
1359
- except: 1,
1360
- minus: 1,
1361
- intersect: 2,
1362
- };
1363
-
1364
- // Create AST node for binary operator `op` and arguments `args`
1365
- function leftAssocBinaryOp( expr, right, opToken, eToken, extraProp ) {
1366
- if (!right)
1367
- return expr;
1368
- const op = this.valueWithTokenLocation( opToken.text.toLowerCase(), opToken );
1369
- const extra = eToken
1370
- ? this.valueWithTokenLocation( eToken.text.toLowerCase(), eToken )
1371
- : undefined;
1372
- if (!expr.$parens && expr.op?.val === op.val && expr[extraProp]?.val === extra?.val) {
1373
- expr.args.push( right );
1374
- return expr;
1375
- }
1376
- const opPrec = operatorPrecedences[op.val] || 0;
1377
- let left = expr;
1378
- let args;
1379
- while (opPrec > nodePrecedence( left )) {
1380
- args = left.args;
1381
- left = args[args.length - 1];
1382
- }
1383
- // TODO: location correct?
1384
- const node = (extra) // eslint-disable-next-line
1385
- ? { op, [extraProp]: extra, args: [ left, right ], location: left.location }
1386
- : { op, args: [ left, right ], location: left.location };
1387
- if (!args)
1388
- return node;
1389
- args[args.length - 1] = node;
1390
- return expr;
1391
- }
1392
-
1393
- function nodePrecedence( node ) {
1394
- const { op } = node;
1395
- return op && !node.$parens && operatorPrecedences[op.val] || Infinity;
1396
- }
1397
-
1398
- function pushOpToken( args, precedence ) { // for nary only; uses LT(-1) as operator token
1399
- let node = null;
1400
- let left = args;
1401
- while (left?.$opPrecedence && left.$opPrecedence < precedence) {
1402
- args = left;
1403
- node = args[args.length - 1]; // last sub node of left side
1404
- left = node.args;
1405
- }
1406
-
1407
- if (left?.$opPrecedence === precedence ) { // nary
1408
- args = left;
1409
- }
1410
- else if (node) {
1411
- const sub = this.argsExpression( [ node, null ], true );
1412
- args[args.length - 1] = sub;
1413
- args = sub.args;
1414
- args.length = 1;
1415
- }
1416
- else if (args.length > 1) { // new top-level op & op on left
1417
- args[0] = this.argsExpression( [ ...args ], args.$opPrecedence != null ); // finish expresion
1418
- args.length = 1;
1419
- }
1420
- args.$opPrecedence = precedence;
1421
- // TODO (if necessary): `location` for sub expessions, top-level is be properly set
1422
- this.pushXprToken( args );
1423
- return args;
1424
- }
1425
-
1426
- // only to be used in @after or via pushOpToken
1427
- function argsExpression( args, nary ) {
1428
- if (args.length === 1) // args.length === 0 is ok (for OVER…)
1429
- return args[0];
1430
- const $parens = args[0]?.$parens;
1431
- const loc = ($parens) ? $parens[$parens.length - 1] : args[0]?.location;
1432
- const location = loc ? { __proto__: Location.prototype, ...loc } : this.startLocation();
1433
- // console.log('AE:',args);
1434
- const op = {
1435
- // eslint-disable-next-line no-nested-ternary
1436
- val: nary === '?:' ? nary : nary ? 'nary' : 'ixpr',
1437
- location,
1438
- };
1439
- return this.attachLocation( { op, args, location } );
1440
- }
1441
-
1442
- const maxCardinalityKeywords = { 1: 'one', '*': 'many' };
1443
-
1444
- function setMaxCardinality( art, targetMax, token ) { // - val
1445
- if (token)
1446
- targetMax.location = this.tokenLocation( token );
1447
- if (art.cardinality) {
1448
- this.reportDuplicateClause( 'cardinality', targetMax, art.cardinality.targetMax,
1449
- maxCardinalityKeywords );
1450
- }
1451
- else {
1452
- art.cardinality = { targetMax, location: targetMax.location };
1453
- }
1454
- }
1455
-
1456
- const notNullKeywords = { false: 'null', true: 'not null' };
1457
-
1458
- function setNullability( art, token1, token2 ) {
1459
- const notNull = this.valueWithTokenLocation( !!token2, token1, token2 );
1460
- if (art.notNull)
1461
- this.reportDuplicateClause( 'notNull', art.notNull, notNull, notNullKeywords );
1462
- art.notNull = notNull;
1463
- }
1464
-
1465
- function reportDuplicateClause( prop, erroneous, chosen, keywords ) {
1466
- // probably easier for message linters not to use (?:) for the message id...?
1467
- const args = {
1468
- '#': prop,
1469
- code: keywords[chosen.val] || chosen.val,
1470
- line: chosen.location.line,
1471
- col: chosen.location.col,
1472
- };
1473
- if (erroneous.val === chosen.val)
1474
- this.warning( 'syntax-duplicate-equal-clause', erroneous.location, args );
1475
- else
1476
- this.message( 'syntax-duplicate-clause', erroneous.location, args );
1477
- }
1478
-
1479
- const extensionsCode = {
1480
- definitions: 'extend … with definitions',
1481
- context: 'extend context',
1482
- service: 'extend service',
1483
- };
1484
-
1485
- function reportUnexpectedExtension( defOnly, token ) {
1486
- if (defOnly) {
1487
- this.error( 'syntax-unexpected-extension', token,
1488
- { keyword: token.text, code: extensionsCode[defOnly] } );
1489
- }
1490
- }
1491
-
1492
- function handleComposition( cardinality, isComposition ) {
1493
- if (isComposition && !cardinality) {
1494
- const lt1 = this._input.LT(1).type;
1495
- const la2 = this._input.LT(2);
1496
- if (la2.text === '{' && (lt1 === this.constructor.MANY || lt1 === this.constructor.ONE))
1497
- la2.type = this.constructor.COMPOSITIONofBRACE;
1498
- }
1499
- const brace1 = (isComposition) ? 'COMPOSITIONofBRACE' : "'{'";
1500
- const manyOne = (cardinality) ? [ 'MANY', 'ONE' ] : [];
1501
- this.excludeExpected( [ [ "'}'", 'COMPOSITIONofBRACE' ], brace1, ...manyOne ] );
1502
- }
1503
-
1504
- function associationInSelectItem( art ) {
1505
- const { value } = art;
1506
- const path = value?.path;
1507
- // we cannot compare "just one token before `:`" because there might be annos
1508
- if (path && path.length === 1 && !art.name && !art.expand && !art.inline) {
1509
- const name = value.path[0];
1510
- if (path.length === 1 && !name.args && !name.cardinality && !name.where) {
1511
- art.name = name;
1512
- delete art.value;
1513
- return art;
1514
- }
1515
- }
1516
- this.error( 'syntax-unexpected-assoc', this.getCurrentToken(), {},
1517
- 'Unexpected association definition in select item' );
1518
- return {}; // result of the association rules are written into /dev/null
1519
- }
1520
-
1521
- function reportExpandInline( column, isInline ) {
1522
- const { name } = column;
1523
- if (column.value && !column.value.path) {
1524
- let token = this.getCurrentToken();
1525
- // improve error location when using "inline" `.{…}` after ref (arguments and
1526
- // filters not covered, not worth the effort); after an expression where
1527
- // the last token is an identifier, not the `.` is wrong, but the `{`:
1528
- if (isInline && !name && this._input.LT(-1).type >= this.constructor.Identifier)
1529
- token = this._input.LT(2);
1530
- this.error( 'syntax-unexpected-nested-proj', token,
1531
- { code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }' },
1532
- 'Unexpected $(CODE); nested projections can only be used after a reference' );
1533
- // continuation semantics:
1534
- // - add elements anyway (could lead to duplicate errors as usual)
1535
- // - no errors for refs inside expand/inline, but for refs in sibling expr
1536
- // - think about: reference to these (sub) elements from other view
1537
- }
1538
- if (isInline && name) {
1539
- const location = this.tokenLocation( isInline, this._input.LT(-1) );
1540
- this.error( 'syntax-unexpected-alias', location, { code: '.{ ‹inline› }' },
1541
- 'Unexpected alias name before $(CODE)' );
1542
- // continuation semantics: ignore AS
1543
- }
1544
- }
1545
-
1546
- function checkTypeFacet( art, argIdent ) {
1547
- // TODO: use dictAddArray or dictAdd?
1548
- const { id } = argIdent;
1549
- if (id === 'length' || id === 'scale' || id === 'precision' || id === 'srid') {
1550
- if (art[id] !== undefined) {
1551
- this.error( 'syntax-duplicate-argument', art[id].location,
1552
- { '#': 'type', name: id } );
1553
- // continuation semantics: use last
1554
- }
1555
- return true;
1556
- }
1557
- this.error( 'syntax-undefined-param', argIdent.location, { name: id },
1558
- 'There is no type parameter called $(NAME)');
1559
- return false;
1560
- }
1561
-
1562
- function checkTypeArgs( art ) {
1563
- const args = art.$typeArgs;
1564
- // One or two arguments are interpreted as either length or precision/scale.
1565
- if (args.length > 2) {
1566
- const loc = args[2].location;
1567
- this.error( 'syntax-unexpected-argument', loc, {}, 'Too many type arguments' );
1568
- art.$typeArgs = undefined;
1569
- }
1570
- }
1571
-
1572
- module.exports = GenericAntlrParser;