@sap/cds-compiler 5.4.4 → 5.5.2
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.
- package/CHANGELOG.md +22 -1
- package/bin/cds_remove_invalid_whitespace.js +4 -4
- package/bin/cds_update_annotations.js +3 -3
- package/bin/cds_update_identifiers.js +3 -3
- package/lib/api/main.js +18 -30
- package/lib/api/validate.js +6 -1
- package/lib/base/lazyload.js +28 -0
- package/lib/base/location.js +1 -0
- package/lib/base/message-registry.js +47 -11
- package/lib/base/messages.js +17 -3
- package/lib/checks/cdsMap.js +27 -0
- package/lib/checks/{dbFeatureFlags.js → featureFlags.js} +1 -1
- package/lib/checks/parameters.js +61 -4
- package/lib/checks/validator.js +17 -7
- package/lib/compiler/define.js +1 -0
- package/lib/compiler/index.js +7 -7
- package/lib/gen/BaseParser.js +345 -235
- package/lib/gen/CdlParser.js +4438 -4492
- package/lib/gen/Dictionary.json +2 -2
- package/lib/language/antlrParser.js +2 -111
- package/lib/main.js +16 -37
- package/lib/model/cloneCsn.js +1 -5
- package/lib/modelCompare/utils/filter.js +47 -21
- package/lib/parsers/AstBuildingParser.js +92 -73
- package/lib/parsers/CdlGrammar.g4 +110 -137
- package/lib/parsers/index.js +123 -0
- package/lib/render/toSql.js +8 -2
- package/lib/render/utils/delta.js +33 -1
- package/lib/transform/db/{transformExists.js → assocsToQueries/transformExists.js} +12 -407
- package/lib/transform/db/assocsToQueries/utils.js +440 -0
- package/lib/transform/db/expansion.js +2 -2
- package/lib/transform/draft/db.js +14 -3
- package/lib/transform/effective/annotations.js +3 -3
- package/lib/transform/effective/main.js +5 -7
- package/lib/transform/featureFlags.js +5 -0
- package/lib/transform/forRelationalDB.js +125 -192
- package/lib/transform/transformUtils.js +0 -51
- package/lib/utils/objectUtils.js +13 -0
- package/package.json +2 -2
- package/lib/transform/db/featureFlags.js +0 -5
|
@@ -70,37 +70,34 @@ class AstBuildingParser extends BaseParser {
|
|
|
70
70
|
return this.$messageFunctions.info( id, location?.location || location, args, text );
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
expectingArray(
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
expectingArray() {
|
|
74
|
+
const savedState = this.s;
|
|
75
|
+
this.s = this.errorState;
|
|
76
|
+
let array = this.expectingArray_();
|
|
77
|
+
this.s = savedState;
|
|
78
|
+
// compatibility: replace true+false by Boolean - TODO: delete
|
|
79
|
+
if (array.includes( 'true' ))
|
|
78
80
|
array = [ 'Boolean', ...array.filter( n => n !== 'true' && n !== 'false' ) ];
|
|
79
|
-
return array.map( antlrName )
|
|
81
|
+
return array.map( tok => this.antlrName( tok ) )
|
|
80
82
|
.sort( (a, b) => (tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1) );
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
reportUnexpectedToken_(
|
|
84
|
-
const
|
|
85
|
+
reportUnexpectedToken_() {
|
|
86
|
+
const token = this.la();
|
|
87
|
+
const expecting = this.expectingArray();
|
|
85
88
|
const err = this.error( 'syntax-unexpected-token', token,
|
|
86
|
-
{ offending: antlrName( token ), expecting } );
|
|
89
|
+
{ offending: this.antlrName( token ), expecting } );
|
|
87
90
|
// No 'unwanted' variant, no 'syntax-missing-token'
|
|
88
91
|
err.expectedTokens = expecting;
|
|
89
92
|
}
|
|
90
|
-
reportReservedWord_(
|
|
93
|
+
reportReservedWord_() {
|
|
94
|
+
const token = this.la();
|
|
91
95
|
const err = this.message( 'syntax-unexpected-reserved-word', token,
|
|
92
96
|
{ code: token.text, delimited: token.text } );
|
|
93
97
|
// TODO: at least if one expected keyword is similar, mention expected set
|
|
94
98
|
err.expectedTokens = this.expectingArray();
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
reportInternalError_( token ) {
|
|
98
|
-
this.error( null, token, { offending: antlrName( token ) },
|
|
99
|
-
'Mismatched $(OFFENDING); skipped one token' );
|
|
100
|
-
// TMP: should not happen anymore → remove method in redepage
|
|
101
|
-
throw new Error( 'Repeated error reporting with same token' );
|
|
102
|
-
}
|
|
103
|
-
|
|
104
101
|
tableWithoutAs() {
|
|
105
102
|
// TODO TOOL: if the tool properly creates `default: this.giR()`, this
|
|
106
103
|
// condition method is most likely not necessary
|
|
@@ -180,7 +177,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
180
177
|
return this.lGenericIntroOrExpr( false );
|
|
181
178
|
}
|
|
182
179
|
|
|
183
|
-
lGenericSeparator() {
|
|
180
|
+
lGenericSeparator() { // TODO: { keyword, type } as arg ?
|
|
184
181
|
const { keyword, type } = this.la();
|
|
185
182
|
// TODO: use lower-case in specialFunctions
|
|
186
183
|
const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
|
|
@@ -191,7 +188,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
191
188
|
translateParserToken_( tokenName ) {
|
|
192
189
|
const realTokens = this.dynamic_.generic?.[parserTokens[tokenName]];
|
|
193
190
|
// TODO: avoid parserTokens dict, use lower-case in specialFunctions
|
|
194
|
-
return realTokens?.map( s => s.toLowerCase() ) ?? [];
|
|
191
|
+
return realTokens?.map( s => s.toLowerCase() ) ?? [ tokenName ];
|
|
195
192
|
}
|
|
196
193
|
|
|
197
194
|
inSelectItem( _test, arg ) { // only as action
|
|
@@ -211,14 +208,14 @@ class AstBuildingParser extends BaseParser {
|
|
|
211
208
|
return true;
|
|
212
209
|
// TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
|
|
213
210
|
// (as <prepare>)
|
|
214
|
-
const next = this.tokens[this.tokenIdx + 1]
|
|
211
|
+
const next = this.tokens[this.tokenIdx + 1]?.type;
|
|
215
212
|
return next !== '*' && next !== '{';
|
|
216
213
|
}
|
|
217
214
|
|
|
218
215
|
// <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
|
|
219
216
|
// completion for `… default 3 not ~;` → currently just `null` but hey
|
|
220
217
|
isNegatedRelation( _test, prec ) {
|
|
221
|
-
return this.tokens[this.tokenIdx + 1]
|
|
218
|
+
return this.tokens[this.tokenIdx + 1]?.keyword !== 'null' &&
|
|
222
219
|
this.precNone_( _test, prec );
|
|
223
220
|
}
|
|
224
221
|
|
|
@@ -261,13 +258,13 @@ class AstBuildingParser extends BaseParser {
|
|
|
261
258
|
*
|
|
262
259
|
* Called as <prepare=…>:
|
|
263
260
|
*
|
|
264
|
-
* - <…, arg=default> in `returnsSpec`: after `returns`
|
|
265
|
-
* disallow `default` in `typeExpression`
|
|
266
261
|
* - <…,arg=elem> in `elementDef` (before calling `typeExpression`):
|
|
267
262
|
* allow `default`/`= calcExpr` with final annotation assignments,
|
|
268
263
|
* delay final doc comment
|
|
264
|
+
* - <…, arg=default> in `returnsSpec`: after `returns`
|
|
265
|
+
* disallow `default` in `typeExpression`
|
|
269
266
|
* - <…, arg=calc> in `typeExpression` (with associations, etc)
|
|
270
|
-
* now disallow `= calcExpr`
|
|
267
|
+
* now disallow `= calcExpr` in `elementDef`,
|
|
271
268
|
* do not delay final doc comments anymore
|
|
272
269
|
* - <…, arg=anno> in `typeExpression` after enums:
|
|
273
270
|
* now disallow annotation assignments after `= calcExpr`,
|
|
@@ -275,25 +272,48 @@ class AstBuildingParser extends BaseParser {
|
|
|
275
272
|
*
|
|
276
273
|
* Called as <cond=…>:
|
|
277
274
|
*
|
|
278
|
-
* - <…, arg=default> in `
|
|
279
|
-
* is `default` allowed? If used, disallow calc
|
|
275
|
+
* - <…, arg=default> in `typeExpression` and `typeProperties`
|
|
276
|
+
* is `default` allowed? If used, disallow calc and further DEFAULT
|
|
277
|
+
* - <…, arg=notNull> in `typeExpression` and `typeProperties`
|
|
278
|
+
* is `null`/`not null` allowed? ensures that it is only used once
|
|
280
279
|
* - <…, arg=calc> in `elementDef`:
|
|
281
|
-
* is `= calcExpr` allowed?
|
|
280
|
+
* is `= calcExpr` allowed? not with struct, assoc or MANY…
|
|
282
281
|
* - <…, arg=anno> in `elementDef`:
|
|
283
|
-
* are annotation assignments after `= calcExpr` allowed?
|
|
282
|
+
* are annotation assignments after `= calcExpr` allowed? not with ENUM…
|
|
283
|
+
*
|
|
284
|
+
* The value of the dynamic var `elementCtx` looks like [REJECTED, DEFAULT,
|
|
285
|
+
* NOTNULL] where
|
|
286
|
+
*
|
|
287
|
+
* - REJECTED is the string containing a to-be-rejected test `arg`
|
|
288
|
+
* - DEFAULT: true if `default` had been provided
|
|
289
|
+
* - NOTNULL: true if `null` or `not null` had been provided
|
|
284
290
|
*/
|
|
285
291
|
elementRestriction( test, arg ) {
|
|
286
|
-
|
|
292
|
+
let { elementCtx } = this.dynamic_;
|
|
287
293
|
if (test) {
|
|
288
|
-
if (
|
|
289
|
-
return
|
|
294
|
+
if (elementCtx?.[0] === arg)
|
|
295
|
+
return false;
|
|
296
|
+
if (!elementCtx) { // with type, param, or annotation defs
|
|
297
|
+
// eslint-disable-next-line no-multi-assign
|
|
298
|
+
elementCtx = this.dynamic_.elementCtx = [ null, false, false ];
|
|
299
|
+
}
|
|
290
300
|
if (arg === 'default') {
|
|
301
|
+
if (elementCtx[1])
|
|
302
|
+
return false;
|
|
303
|
+
elementCtx[1] = true;
|
|
291
304
|
elementCtx[0] = 'calc';
|
|
292
305
|
this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
|
|
293
306
|
}
|
|
307
|
+
else if (arg === 'notNull') {
|
|
308
|
+
if (elementCtx[2]) {
|
|
309
|
+
if (this.la().keyword !== elementCtx[2] || test === 'M') // TODO v6: always error
|
|
310
|
+
return false; // error if different nullibility specification
|
|
311
|
+
}
|
|
312
|
+
elementCtx[2] = this.la().keyword;
|
|
313
|
+
}
|
|
294
314
|
}
|
|
295
315
|
else if (arg === 'elem' || arg === 'default') {
|
|
296
|
-
this.dynamic_.elementCtx = [ arg ];
|
|
316
|
+
this.dynamic_.elementCtx = [ arg, false, false ];
|
|
297
317
|
}
|
|
298
318
|
else if (elementCtx) {
|
|
299
319
|
elementCtx[0] = arg;
|
|
@@ -301,10 +321,6 @@ class AstBuildingParser extends BaseParser {
|
|
|
301
321
|
return true;
|
|
302
322
|
}
|
|
303
323
|
|
|
304
|
-
inExpandInline() { // not as <cond>
|
|
305
|
-
this.dynamic_.inSelectItem = 'nested';
|
|
306
|
-
}
|
|
307
|
-
|
|
308
324
|
/**
|
|
309
325
|
* `;` between statements is optional only after a `}` (ex braces of structure
|
|
310
326
|
* values for annotations).
|
|
@@ -338,8 +354,11 @@ class AstBuildingParser extends BaseParser {
|
|
|
338
354
|
const { arrayAnno } = this.dynamic_;
|
|
339
355
|
if (!arrayAnno[0])
|
|
340
356
|
return false;
|
|
341
|
-
|
|
342
|
-
|
|
357
|
+
arrayAnno[0] = this.tokens[this.tokenIdx + 1]?.keyword;
|
|
358
|
+
}
|
|
359
|
+
else if (arg === 'bracket') {
|
|
360
|
+
// closing bracket not allowed if last `...` in array is with `up to
|
|
361
|
+
return typeof this.dynamic_.arrayAnno[0] !== 'string';
|
|
343
362
|
}
|
|
344
363
|
else { // orNotEmpty
|
|
345
364
|
return this.dynamic_.arrayAnno || this.lb().type !== '{';
|
|
@@ -737,22 +756,10 @@ class AstBuildingParser extends BaseParser {
|
|
|
737
756
|
* - misplaced doc comments would lead to a parse error (incompatible),
|
|
738
757
|
* - would influence the prediction and error recovery,
|
|
739
758
|
* - is only slightly "more declarative" in the grammar.
|
|
740
|
-
*
|
|
741
|
-
* With argument `delayed`, potentially delay the doc processing.
|
|
742
|
-
* See also `elementRestriction`.
|
|
743
759
|
*/
|
|
744
|
-
docComment( art
|
|
745
|
-
if (delayed !== this.dynamic_?.[0]) {
|
|
746
|
-
if (delayed === 'type')
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
else if (delayed === 'elem') {
|
|
750
|
-
this.dynamic_[0] = 'type';
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
760
|
+
docComment( art ) {
|
|
754
761
|
const { line: prevLine, col: prevCol } = this.lb()?.location ?? { line: 0, col: 0 };
|
|
755
|
-
const { line: currLine, col: currCol } = this.la().location;
|
|
762
|
+
const { line: currLine, col: currCol } = (this.la() ?? this.lb()).location;
|
|
756
763
|
let token;
|
|
757
764
|
for (;;) {
|
|
758
765
|
token = this.docComments[this.docCommentIndex];
|
|
@@ -783,16 +790,20 @@ class AstBuildingParser extends BaseParser {
|
|
|
783
790
|
}
|
|
784
791
|
}
|
|
785
792
|
|
|
793
|
+
// TODO: can we remove `;`/EOF from the expected-set for `annotate Foo with ⎀`?
|
|
786
794
|
checkWith( keyword ) {
|
|
787
795
|
if (this.lb() !== keyword)
|
|
788
796
|
return;
|
|
789
797
|
const tok = this.la();
|
|
790
|
-
|
|
791
|
-
|
|
798
|
+
const docTokenIndex = this.docCommentIndex &&
|
|
799
|
+
this.docComments[this.docCommentIndex - 1].location.tokenIndex;
|
|
800
|
+
if (docTokenIndex < tok.location.tokenIndex &&
|
|
801
|
+
docTokenIndex > this.lb().location.tokenIndex)
|
|
792
802
|
return;
|
|
793
|
-
|
|
803
|
+
// filter out what comes after current rule (no generic way necessary):
|
|
804
|
+
const expecting = this.expectingArray().filter( t => t !== '<EOF>' && t !== '\'}\'' );
|
|
794
805
|
const msg = this.warning( 'syntax-unexpected-semicolon', tok,
|
|
795
|
-
{ offending: antlrName( tok ), expecting, keyword: 'with' },
|
|
806
|
+
{ offending: this.antlrName( tok ), expecting, keyword: 'with' },
|
|
796
807
|
// eslint-disable-next-line @stylistic/js/max-len
|
|
797
808
|
'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
|
|
798
809
|
msg.expectedTokens = expecting;
|
|
@@ -801,13 +812,16 @@ class AstBuildingParser extends BaseParser {
|
|
|
801
812
|
setNullability( art, val, location = this.lb().location ) {
|
|
802
813
|
const notNull = { val, location };
|
|
803
814
|
if (art.notNull) {
|
|
804
|
-
|
|
815
|
+
// complain about the second
|
|
816
|
+
this.reportDuplicateClause( 'notNull', notNull, art.notNull,
|
|
805
817
|
(val ? 'not null' : 'null') );
|
|
806
818
|
}
|
|
807
|
-
|
|
819
|
+
else {
|
|
820
|
+
art.notNull = notNull;
|
|
821
|
+
}
|
|
808
822
|
}
|
|
809
823
|
|
|
810
|
-
setAssocAndComposition( art, assoc, card, target ) {
|
|
824
|
+
setAssocAndComposition( art, assoc, card, target = {} ) {
|
|
811
825
|
const { location } = assoc;
|
|
812
826
|
art.type = {
|
|
813
827
|
path: [ { id: keywordTypeNames[assoc.keyword], location } ],
|
|
@@ -816,7 +830,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
816
830
|
};
|
|
817
831
|
art.target = target;
|
|
818
832
|
if (!card)
|
|
819
|
-
return;
|
|
833
|
+
return target;
|
|
820
834
|
|
|
821
835
|
const targetMax = (card.keyword === 'one')
|
|
822
836
|
? { val: 1, literal: 'number', location: card.location }
|
|
@@ -829,17 +843,19 @@ class AstBuildingParser extends BaseParser {
|
|
|
829
843
|
else {
|
|
830
844
|
art.cardinality = { targetMax, location: targetMax.location };
|
|
831
845
|
}
|
|
846
|
+
return target;
|
|
832
847
|
}
|
|
833
848
|
|
|
834
849
|
reportExpandInline( column, isInline ) {
|
|
850
|
+
// called before matching `{`
|
|
835
851
|
const { name } = column;
|
|
836
852
|
if (column.value && !column.value.path) {
|
|
837
|
-
const token = this.la();
|
|
838
853
|
// improve error location when using "inline" `.{…}` after ref (arguments and
|
|
839
854
|
// filters not covered, not worth the effort); after an expression where
|
|
840
855
|
// the last token is an identifier, not the `.` is wrong, but the `{`:
|
|
841
|
-
|
|
842
|
-
|
|
856
|
+
const token = (isInline && this.tokens[this.tokenIdx - 2].type !== 'Id')
|
|
857
|
+
? this.lb()
|
|
858
|
+
: this.la();
|
|
843
859
|
this.error( 'syntax-unexpected-nested-proj', token,
|
|
844
860
|
{ code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }' },
|
|
845
861
|
'Unexpected $(CODE); nested projections can only be used after a reference' );
|
|
@@ -868,9 +884,10 @@ class AstBuildingParser extends BaseParser {
|
|
|
868
884
|
col: chosen.location.col,
|
|
869
885
|
};
|
|
870
886
|
if (erroneous.val === chosen.val) {
|
|
887
|
+
// TODO v6: duplicate clause = error, independently whether it is the same
|
|
871
888
|
this.warning( 'syntax-duplicate-equal-clause', erroneous.location, args );
|
|
872
889
|
}
|
|
873
|
-
else {
|
|
890
|
+
else if (prop !== 'notNull') { // already via guard in grammar
|
|
874
891
|
if (literalValIfNotEq)
|
|
875
892
|
args.code = chosen.val;
|
|
876
893
|
this.message( 'syntax-duplicate-clause', erroneous.location, args );
|
|
@@ -1277,6 +1294,17 @@ class AstBuildingParser extends BaseParser {
|
|
|
1277
1294
|
parent, 'elements', kind, seg );
|
|
1278
1295
|
}
|
|
1279
1296
|
}
|
|
1297
|
+
|
|
1298
|
+
// For compatibility with ANTLR-based parser:
|
|
1299
|
+
antlrName( type ) {
|
|
1300
|
+
if (typeof type !== 'string') {
|
|
1301
|
+
type = (!type.parsedAs && this.keywords[type.keyword ?? ''] != null ||
|
|
1302
|
+
type.parsedAs === 'keyword') && type.keyword || type.type;
|
|
1303
|
+
}
|
|
1304
|
+
if (/^[A-Z]+/.test( type ))// eslint-disable-next-line no-nested-ternary
|
|
1305
|
+
return (type === 'Id') ? 'Identifier' : (type === 'EOF') ? '<EOF>' : type;
|
|
1306
|
+
return (/^[a-z]+/.test( type )) ? type.toUpperCase() : `'${ type }'`;
|
|
1307
|
+
}
|
|
1280
1308
|
}
|
|
1281
1309
|
|
|
1282
1310
|
function addOneForDefinition( count, ext ) {
|
|
@@ -1299,15 +1327,6 @@ function relevantDigits( val ) {
|
|
|
1299
1327
|
}
|
|
1300
1328
|
|
|
1301
1329
|
|
|
1302
|
-
// For compatibility with ANTLR-based parser:
|
|
1303
|
-
function antlrName( type ) {
|
|
1304
|
-
if (typeof type !== 'string')
|
|
1305
|
-
type = (!type.parsedAs || type.parsedAs === 'keyword') && type.keyword || type.type;
|
|
1306
|
-
if (/^[A-Z]+/.test( type ))// eslint-disable-next-line no-nested-ternary
|
|
1307
|
-
return (type === 'Id') ? 'Identifier' : (type === 'EOF') ? '<EOF>' : type;
|
|
1308
|
-
return (/^[a-z]+/.test( type )) ? type.toUpperCase() : `'${ type }'`;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
1330
|
// Used for sorting in messages (TODO: make it part of messages.js?)
|
|
1312
1331
|
const token1sort = {
|
|
1313
1332
|
// 0: Identifier, Number, ...
|