@sap/cds-compiler 5.3.2 → 5.4.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.
- package/CHANGELOG.md +23 -2
- package/bin/cdsc.js +1 -1
- package/doc/CHANGELOG_BETA.md +2 -2
- package/lib/api/options.js +4 -2
- package/lib/base/builtins.js +0 -10
- package/lib/base/keywords.js +3 -31
- package/lib/base/message-registry.js +23 -5
- package/lib/base/messages.js +1 -1
- package/lib/checks/existsMustEndInAssoc.js +7 -2
- package/lib/checks/foreignKeys.js +12 -7
- package/lib/compiler/assert-consistency.js +11 -3
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/checks.js +88 -38
- package/lib/compiler/define.js +2 -2
- package/lib/compiler/shared.js +9 -10
- package/lib/compiler/xpr-rewrite.js +11 -0
- package/lib/compiler/xsn-model.js +1 -1
- package/lib/edm/csn2edm.js +2 -0
- package/lib/edm/edm.js +2 -1
- package/lib/edm/edmPreprocessor.js +14 -1
- package/lib/edm/edmUtils.js +17 -2
- package/lib/gen/BaseParser.js +291 -197
- package/lib/gen/CdlParser.js +1631 -1605
- package/lib/gen/Dictionary.json +74 -6
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +1808 -1804
- package/lib/language/antlrParser.js +8 -4
- package/lib/language/genericAntlrParser.js +3 -3
- package/lib/model/csnUtils.js +6 -1
- package/lib/optionProcessor.js +4 -0
- package/lib/parsers/AstBuildingParser.js +172 -108
- package/lib/parsers/CdlGrammar.g4 +154 -134
- package/lib/parsers/Lexer.js +3 -3
- package/lib/parsers/identifiers.js +59 -0
- package/lib/render/toCdl.js +5 -5
- package/lib/render/utils/common.js +5 -0
- package/lib/render/utils/delta.js +23 -5
- package/lib/transform/db/expansion.js +2 -1
- package/lib/transform/db/transformExists.js +10 -9
- package/lib/transform/effective/annotations.js +147 -0
- package/lib/transform/effective/main.js +16 -2
- package/lib/transform/forOdata.js +53 -10
- package/lib/transform/forRelationalDB.js +7 -0
- package/lib/transform/odata/createForeignKeys.js +180 -0
- package/lib/transform/odata/flattening.js +135 -19
- package/lib/transform/odata/typesExposure.js +4 -3
- package/lib/transform/transformUtils.js +6 -6
- package/package.json +1 -1
|
@@ -275,15 +275,19 @@ function parseWithNewParser( source, filename, options, messageFunctions, rule )
|
|
|
275
275
|
// TODO: what should we do in case of errors?
|
|
276
276
|
$ctx.stop = this.lb();
|
|
277
277
|
parseListener.exitEveryRule( $ctx );
|
|
278
|
-
CdlParser.prototype.exit_.apply( this, args );
|
|
278
|
+
return CdlParser.prototype.exit_.apply( this, args );
|
|
279
279
|
};
|
|
280
280
|
parser.c = function c( ...args ) { // consume
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
const symbol = this.la();
|
|
282
|
+
const result = CdlParser.prototype.c.apply( this, args );
|
|
283
|
+
if (result)
|
|
284
|
+
parseListener.visitTerminal( { symbol } );
|
|
285
|
+
return result;
|
|
283
286
|
};
|
|
284
287
|
parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery
|
|
288
|
+
const symbol = this.la();
|
|
285
289
|
CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx`
|
|
286
|
-
parseListener.visitErrorNode( { symbol
|
|
290
|
+
parseListener.visitErrorNode( { symbol } );
|
|
287
291
|
};
|
|
288
292
|
}
|
|
289
293
|
const result = {};
|
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
specialFunctions,
|
|
18
18
|
quotedLiteralPatterns,
|
|
19
19
|
} = require('../compiler/builtins');
|
|
20
|
-
const {
|
|
20
|
+
const { functionsWithoutParentheses } = require('../parsers/identifiers');
|
|
21
21
|
const { Location } = require('../base/location');
|
|
22
22
|
const { pathName } = require('../compiler/utils');
|
|
23
23
|
const { XsnArtifact, XsnName, XsnSource } = require('../compiler/xsn-model');
|
|
@@ -728,7 +728,7 @@ function docComment( node ) {
|
|
|
728
728
|
*/
|
|
729
729
|
function classifyImplicitName( category, ref ) {
|
|
730
730
|
if (!ref || ref.path) {
|
|
731
|
-
const tokenIndex = ref?.path
|
|
731
|
+
const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
|
|
732
732
|
const implicit = (tokenIndex === undefined) ? this._input.LT(-1) : this._input.get(tokenIndex);
|
|
733
733
|
if (implicit.isIdentifier) {
|
|
734
734
|
const previous = implicit.isIdentifier;
|
|
@@ -847,7 +847,7 @@ function valuePathAst( ref ) {
|
|
|
847
847
|
const { args, id, location } = path[0];
|
|
848
848
|
if (args
|
|
849
849
|
? path[0].$syntax === ':'
|
|
850
|
-
: path[0].$delimited || !
|
|
850
|
+
: path[0].$delimited || !functionsWithoutParentheses.includes( id.toUpperCase() ))
|
|
851
851
|
return ref;
|
|
852
852
|
|
|
853
853
|
const implicit = this.previousTokenAtLocation( location );
|
package/lib/model/csnUtils.js
CHANGED
|
@@ -227,7 +227,12 @@ function getUtils( model, universalReady ) {
|
|
|
227
227
|
* @returns {boolean}
|
|
228
228
|
*/
|
|
229
229
|
function isStructured( obj ) {
|
|
230
|
-
|
|
230
|
+
if (obj.elements)
|
|
231
|
+
return true;
|
|
232
|
+
if (!obj.type)
|
|
233
|
+
return false;
|
|
234
|
+
const typeInfo = getFinalTypeInfo(obj.type);
|
|
235
|
+
return !!(typeInfo?.elements); // TODO? `|| typeInfo?.type === 'cds.Map');`
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
// Return true if 'node' is a managed association element
|
package/lib/optionProcessor.js
CHANGED
|
@@ -603,6 +603,7 @@ optionProcessor.command('forEffective')
|
|
|
603
603
|
optionProcessor.command('forSeal')
|
|
604
604
|
.option('-h, --help')
|
|
605
605
|
.option('--remap-odata-annotations <val>', { valid: ['true', 'false'] } )
|
|
606
|
+
.option('--derive-analytical-annotations <val>', { valid: ['true', 'false'] })
|
|
606
607
|
.positionalArgument('<files...>')
|
|
607
608
|
.help(`
|
|
608
609
|
Usage: cdsc forSeal [options] <files...>
|
|
@@ -614,6 +615,9 @@ optionProcessor.command('forSeal')
|
|
|
614
615
|
--remap-odata-annotations <val> Remap OData annotations to ABAP annotations:
|
|
615
616
|
true: (default) remap annotations
|
|
616
617
|
false: leave them as is
|
|
618
|
+
--derive-analytical-annotations <val> Set analytics annotations
|
|
619
|
+
true: set the annotations
|
|
620
|
+
false: (default) don't set them
|
|
617
621
|
`);
|
|
618
622
|
|
|
619
623
|
module.exports = {
|
|
@@ -4,7 +4,7 @@ const BaseParser = require( '../gen/BaseParser' );
|
|
|
4
4
|
|
|
5
5
|
const { Location } = require( '../base/location' );
|
|
6
6
|
const { dictAdd, dictAddArray } = require('../base/dictionaries');
|
|
7
|
-
const {
|
|
7
|
+
const { functionsWithoutParentheses } = require('./identifiers');
|
|
8
8
|
|
|
9
9
|
const { pathName } = require('../compiler/utils');
|
|
10
10
|
const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
|
|
@@ -70,8 +70,8 @@ class AstBuildingParser extends BaseParser {
|
|
|
70
70
|
return this.$messageFunctions.info( id, location?.location || location, args, text );
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
expectingArray() {
|
|
74
|
-
const expecting = this._expecting();
|
|
73
|
+
expectingArray( token ) {
|
|
74
|
+
const expecting = this._expecting( token );
|
|
75
75
|
let array = Object.keys( expecting );
|
|
76
76
|
// compatibility: replace true+false by Boolean
|
|
77
77
|
if (expecting.true && expecting.false)
|
|
@@ -81,7 +81,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
reportUnexpectedToken_( token ) {
|
|
84
|
-
const expecting = this.expectingArray();
|
|
84
|
+
const expecting = this.expectingArray( token );
|
|
85
85
|
const err = this.error( 'syntax-unexpected-token', token,
|
|
86
86
|
{ offending: antlrName( token ), expecting } );
|
|
87
87
|
// No 'unwanted' variant, no 'syntax-missing-token'
|
|
@@ -94,25 +94,53 @@ class AstBuildingParser extends BaseParser {
|
|
|
94
94
|
err.expectedTokens = this.expectingArray();
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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' );
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
tableWithoutAs() {
|
|
105
|
+
// TODO TOOL: if the tool properly creates `default: this.giR()`, this
|
|
106
|
+
// condition method is most likely not necessary
|
|
104
107
|
const { keyword } = this.la();
|
|
105
|
-
if
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
// TODO: if necessary, we could allow some keywords, and just make sure that
|
|
109
|
+
// all JOIN variants are still possible
|
|
110
|
+
return !keyword || this.keywords[keyword] == null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Handle allowed mixes of expression categories.
|
|
115
|
+
*
|
|
116
|
+
* - <prepare=queryOnLeft>: define a new parentheses context if not direct
|
|
117
|
+
* recursive call, it can finally turn to be both a query or expr/table
|
|
118
|
+
* - <prepare=queryOnLeft, arg=‹SomeVal›>: make the current parentheses
|
|
119
|
+
* context to be not a query anymore
|
|
120
|
+
* - <cond=queryOnLeft> tests whether the expression on the left is a query
|
|
121
|
+
* - <cond=queryOnLeft, arg=‹SomeVal›>: tests whether the expression on the
|
|
122
|
+
* left is a query, then make the current context to be not a query anymore
|
|
123
|
+
* - <cond=queryOnLeft, arg=tableWithoutAs>: …after having checked
|
|
124
|
+
* whether the next token is no (reserved or unreserved) keyword
|
|
125
|
+
*/
|
|
126
|
+
queryOnLeft( test, arg ) {
|
|
127
|
+
if (arg === 'tableWithoutAs') {
|
|
128
|
+
if (!this.tableWithoutAs())
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
else if (!arg && !test) {
|
|
132
|
+
// provide new dynamic parentheses context, except with direct
|
|
133
|
+
// recursive call:
|
|
134
|
+
if (this.inSameRule_( this.s, this.stack.at( -1 ).followState ))
|
|
135
|
+
return true;
|
|
136
|
+
this.dynamic_.parenthesesCtx = [ null ];
|
|
137
|
+
this._tracePush( 'Parentheses()' );
|
|
138
|
+
}
|
|
139
|
+
const { parenthesesCtx } = this.dynamic_;
|
|
140
|
+
const noQuery = parenthesesCtx?.[0];
|
|
141
|
+
if (arg && parenthesesCtx)
|
|
142
|
+
parenthesesCtx[0] = arg;
|
|
143
|
+
return !noQuery;
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
prepareSpecialFunction() {
|
|
@@ -133,7 +161,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
133
161
|
lGenericIntroOrExpr( tryGenericIntro = true ) {
|
|
134
162
|
const { keyword, type } = this.la();
|
|
135
163
|
// TODO: use lower-case in specialFunctions
|
|
136
|
-
const text = keyword
|
|
164
|
+
const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
|
|
137
165
|
const generic = this.dynamic_.generic?.[text];
|
|
138
166
|
if (tryGenericIntro) {
|
|
139
167
|
if (this.dynamic_.generic?.IN === 'separator')
|
|
@@ -142,7 +170,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
142
170
|
return (generic === 'intro') ? 'GenericIntro' : 'Id';
|
|
143
171
|
const next = this.tokens[this.tokenIdx + 1];
|
|
144
172
|
if (next.type !== ',' && next.type !== ')' &&
|
|
145
|
-
this.dynamic_.generic[next.keyword?.toUpperCase()] !== 'separator')
|
|
173
|
+
this.dynamic_.generic[next.keyword?.toUpperCase?.()] !== 'separator')
|
|
146
174
|
return 'GenericIntro';
|
|
147
175
|
}
|
|
148
176
|
return (generic === 'expr') ? 'GenericExpr' : 'Id';
|
|
@@ -155,7 +183,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
155
183
|
lGenericSeparator() {
|
|
156
184
|
const { keyword, type } = this.la();
|
|
157
185
|
// TODO: use lower-case in specialFunctions
|
|
158
|
-
const text = keyword
|
|
186
|
+
const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
|
|
159
187
|
const generic = this.dynamic_.generic?.[text];
|
|
160
188
|
return (generic === 'separator') ? 'GenericSeparator' : ',';
|
|
161
189
|
}
|
|
@@ -166,25 +194,32 @@ class AstBuildingParser extends BaseParser {
|
|
|
166
194
|
return realTokens?.map( s => s.toLowerCase() ) ?? [];
|
|
167
195
|
}
|
|
168
196
|
|
|
169
|
-
|
|
197
|
+
inSelectItem( _test, arg ) { // only as action
|
|
198
|
+
this.dynamic_.inSelectItem = arg;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `virtual` and `key` cannot be used inside expand/inline
|
|
203
|
+
* (also inside sub queries in those, which will be rejected later anyway)
|
|
204
|
+
*/
|
|
205
|
+
modifierRestriction() {
|
|
206
|
+
return this.dynamic_.inSelectItem !== 'nested';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
isDotForPath() { // see also inSelectItem
|
|
170
210
|
if (this.dynamic_.inSelectItem == null)
|
|
171
211
|
return true;
|
|
172
|
-
//
|
|
173
|
-
//
|
|
212
|
+
// TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
|
|
213
|
+
// (as <prepare>)
|
|
174
214
|
const next = this.tokens[this.tokenIdx + 1].type;
|
|
175
215
|
return next !== '*' && next !== '{';
|
|
176
216
|
}
|
|
177
217
|
|
|
178
|
-
// <prec=10, postfix=once> + test that the next token is not `null
|
|
179
|
-
//
|
|
180
|
-
isNegatedRelation() {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.prec_ != null && this.prec_ <= 10 ||
|
|
184
|
-
this.tokens[this.tokenIdx + 1].keyword === 'null')
|
|
185
|
-
return false;
|
|
186
|
-
this.prec_ = 10;
|
|
187
|
-
return true;
|
|
218
|
+
// <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
|
|
219
|
+
// completion for `… default 3 not ~;` → currently just `null` but hey
|
|
220
|
+
isNegatedRelation( _test, prec ) {
|
|
221
|
+
return this.tokens[this.tokenIdx + 1].keyword !== 'null' &&
|
|
222
|
+
this.precNone_( _test, prec );
|
|
188
223
|
}
|
|
189
224
|
|
|
190
225
|
isNamedArg() {
|
|
@@ -223,76 +258,53 @@ class AstBuildingParser extends BaseParser {
|
|
|
223
258
|
|
|
224
259
|
/**
|
|
225
260
|
* Prepare element restrictions and check validility of final anno assignments.
|
|
226
|
-
* TODO TOOL: `arg` for actions/conditions
|
|
227
261
|
*
|
|
228
|
-
* Called
|
|
229
|
-
* - <prepare> after `:` (before calling `typeExpression`):
|
|
230
|
-
* disallow `= calcExpr` and final annotation assignments
|
|
231
|
-
* without further <prepare=calcOrDefaultRestriction>
|
|
232
|
-
* - <prepare> in empty alternative to type expression:
|
|
233
|
-
* allow `= calcExpr` and final annotation assignments
|
|
234
|
-
* - <cond> before final annotation assignments: allowed?
|
|
262
|
+
* Called as <prepare=…>:
|
|
235
263
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Prepare `= calcExpr` restriction and check whether it can be used.
|
|
264
|
+
* - <…, arg=default> in `returnsSpec`: after `returns`
|
|
265
|
+
* disallow `default` in `typeExpression`
|
|
266
|
+
* - <…,arg=elem> in `elementDef` (before calling `typeExpression`):
|
|
267
|
+
* allow `default`/`= calcExpr` with final annotation assignments,
|
|
268
|
+
* delay final doc comment
|
|
269
|
+
* - <…, arg=calc> in `typeExpression` (with associations, etc)
|
|
270
|
+
* now disallow `= calcExpr` (with annotation assignments) in `elementDef`,
|
|
271
|
+
* do not delay final doc comments anymore
|
|
272
|
+
* - <…, arg=anno> in `typeExpression` after enums:
|
|
273
|
+
* now disallow annotation assignments after `= calcExpr`,
|
|
274
|
+
* ignore doc comment after having called `typeExpression`
|
|
251
275
|
*
|
|
252
|
-
* Called
|
|
253
|
-
* - <prepare> before (optionally) calling rule `nullabilityAndDefault`,
|
|
254
|
-
* except for managed associations/compositions:
|
|
255
|
-
* allow `= calcExpr`, allow final annotation assignments if not after `}`
|
|
256
|
-
* (TODO: should we allow `String @anno:{ … } not null @MoreAnnos`?)
|
|
257
|
-
* - <cond> before `default`: disallow calc expr (+ restrict default expr),
|
|
258
|
-
* allowed? (not for `returns`)
|
|
259
|
-
* - <cond> before `=` for calc expressions: allowed?
|
|
276
|
+
* Called as <cond=…>:
|
|
260
277
|
*
|
|
261
|
-
*
|
|
278
|
+
* - <…, arg=default> in `nullabilityAndDefault`:
|
|
279
|
+
* is `default` allowed? If used, disallow calc (with anno assignments)
|
|
280
|
+
* - <…, arg=calc> in `elementDef`:
|
|
281
|
+
* is `= calcExpr` allowed? If so, check for final doc
|
|
282
|
+
* - <…, arg=anno> in `elementDef`:
|
|
283
|
+
* are annotation assignments after `= calcExpr` allowed?
|
|
262
284
|
*/
|
|
263
|
-
|
|
285
|
+
elementRestriction( test, arg ) {
|
|
264
286
|
const { elementCtx } = this.dynamic_;
|
|
265
|
-
if (
|
|
266
|
-
if (!elementCtx)
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
|
|
287
|
+
if (test) {
|
|
288
|
+
if (!elementCtx || elementCtx[0] === arg)
|
|
289
|
+
return !elementCtx;
|
|
290
|
+
if (arg === 'default') {
|
|
291
|
+
elementCtx[0] = 'calc';
|
|
292
|
+
this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
|
|
293
|
+
}
|
|
270
294
|
}
|
|
271
|
-
else if (
|
|
272
|
-
|
|
295
|
+
else if (arg === 'elem' || arg === 'default') {
|
|
296
|
+
this.dynamic_.elementCtx = [ arg ];
|
|
273
297
|
}
|
|
274
|
-
else
|
|
275
|
-
|
|
276
|
-
elementCtx[0] = false;
|
|
277
|
-
this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
|
|
298
|
+
else if (elementCtx) {
|
|
299
|
+
elementCtx[0] = arg;
|
|
278
300
|
}
|
|
279
|
-
return
|
|
301
|
+
return true;
|
|
280
302
|
}
|
|
281
303
|
|
|
282
304
|
inExpandInline() { // not as <cond>
|
|
283
305
|
this.dynamic_.inSelectItem = 'nested';
|
|
284
306
|
}
|
|
285
307
|
|
|
286
|
-
/**
|
|
287
|
-
* `virtual` and `key` cannot be used inside expand/inline
|
|
288
|
-
* (also inside sub queries in those, which will be rejected later anyway)
|
|
289
|
-
*/
|
|
290
|
-
notInExpandInline( test ) {
|
|
291
|
-
if (!test)
|
|
292
|
-
this.dynamic_.inSelectItem = true;
|
|
293
|
-
return this.dynamic_.inSelectItem !== 'nested';
|
|
294
|
-
}
|
|
295
|
-
|
|
296
308
|
/**
|
|
297
309
|
* `;` between statements is optional only after a `}` (ex braces of structure
|
|
298
310
|
* values for annotations).
|
|
@@ -314,20 +326,24 @@ class AstBuildingParser extends BaseParser {
|
|
|
314
326
|
}
|
|
315
327
|
|
|
316
328
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
329
|
+
* - `{}` can only appears inside array-valued annotations
|
|
330
|
+
* - `...` can appear in the top-level array value only and not after `...`
|
|
331
|
+
* without `up to`.
|
|
319
332
|
*/
|
|
320
|
-
|
|
333
|
+
arrayAnno( test, arg ) {
|
|
321
334
|
if (!test) {
|
|
322
335
|
this.dynamic_.arrayAnno = [ !this.dynamic_.arrayAnno ];
|
|
323
336
|
}
|
|
324
|
-
else {
|
|
337
|
+
else if (arg === 'ellipsis') { // on '...'
|
|
325
338
|
const { arrayAnno } = this.dynamic_;
|
|
326
339
|
if (!arrayAnno[0])
|
|
327
340
|
return false;
|
|
328
341
|
if (this.tokens[this.tokenIdx + 1]?.type === ',')
|
|
329
342
|
arrayAnno[0] = false;
|
|
330
343
|
}
|
|
344
|
+
else { // orNotEmpty
|
|
345
|
+
return this.dynamic_.arrayAnno || this.lb().type !== '{';
|
|
346
|
+
}
|
|
331
347
|
return true;
|
|
332
348
|
}
|
|
333
349
|
|
|
@@ -352,9 +368,9 @@ class AstBuildingParser extends BaseParser {
|
|
|
352
368
|
this.error( msgId, location, textArgs );
|
|
353
369
|
}
|
|
354
370
|
|
|
355
|
-
warnIfColonFollows(
|
|
371
|
+
warnIfColonFollows( name ) {
|
|
356
372
|
if (this.l() === ':') {
|
|
357
|
-
this.warning( 'syntax-missing-parens',
|
|
373
|
+
this.warning( 'syntax-missing-parens', name,
|
|
358
374
|
{ code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
|
|
359
375
|
// eslint-disable-next-line @stylistic/js/max-len
|
|
360
376
|
'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
|
|
@@ -370,7 +386,8 @@ class AstBuildingParser extends BaseParser {
|
|
|
370
386
|
}
|
|
371
387
|
}
|
|
372
388
|
|
|
373
|
-
// For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths
|
|
389
|
+
// For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths,
|
|
390
|
+
// inside `.*` and `.{`
|
|
374
391
|
reportUnexpectedSpace( prefix = this.lb(),
|
|
375
392
|
location = this.la().location,
|
|
376
393
|
isError = false ) {
|
|
@@ -469,9 +486,11 @@ class AstBuildingParser extends BaseParser {
|
|
|
469
486
|
}
|
|
470
487
|
if (!prefix) { // set deprecated $annotations for cds-lsp
|
|
471
488
|
const { line, col } = name.location;
|
|
489
|
+
// prefer value end-location if it exists
|
|
490
|
+
const endLoc = val.location || val.name.location;
|
|
472
491
|
const location = {
|
|
473
492
|
__proto__: Location.prototype,
|
|
474
|
-
...
|
|
493
|
+
...endLoc,
|
|
475
494
|
line,
|
|
476
495
|
col,
|
|
477
496
|
};
|
|
@@ -542,7 +561,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
542
561
|
|
|
543
562
|
classifyImplicitName( category, ref ) {
|
|
544
563
|
if (!ref || ref.path) {
|
|
545
|
-
const tokenIndex = ref?.path
|
|
564
|
+
const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
|
|
546
565
|
const token = this.prevTokenWithIndex( tokenIndex ) ?? this.tokens[this.tokenIdx - 1];
|
|
547
566
|
const { parsedAs } = token;
|
|
548
567
|
if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword') {
|
|
@@ -580,7 +599,8 @@ class AstBuildingParser extends BaseParser {
|
|
|
580
599
|
let val;
|
|
581
600
|
|
|
582
601
|
if (text.startsWith( '`' )) {
|
|
583
|
-
val =
|
|
602
|
+
val = token.keyword !== 0 && // 0 -> unterminated literal
|
|
603
|
+
parseMultiLineStringLiteral.call( this, token ); // TODO: remove `call()` syntax
|
|
584
604
|
}
|
|
585
605
|
else {
|
|
586
606
|
pos = text.search( '\'' ) + 1; // pos of char after quote
|
|
@@ -717,8 +737,20 @@ class AstBuildingParser extends BaseParser {
|
|
|
717
737
|
* - misplaced doc comments would lead to a parse error (incompatible),
|
|
718
738
|
* - would influence the prediction and error recovery,
|
|
719
739
|
* - is only slightly "more declarative" in the grammar.
|
|
740
|
+
*
|
|
741
|
+
* With argument `delayed`, potentially delay the doc processing.
|
|
742
|
+
* See also `elementRestriction`.
|
|
720
743
|
*/
|
|
721
|
-
docComment( art ) {
|
|
744
|
+
docComment( art, delayed = undefined ) {
|
|
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
|
+
|
|
722
754
|
const { line: prevLine, col: prevCol } = this.lb()?.location ?? { line: 0, col: 0 };
|
|
723
755
|
const { line: currLine, col: currCol } = this.la().location;
|
|
724
756
|
let token;
|
|
@@ -726,6 +758,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
726
758
|
token = this.docComments[this.docCommentIndex];
|
|
727
759
|
if (!token)
|
|
728
760
|
return; // no further doc comment
|
|
761
|
+
// TODO: we could use location.tokenIndex
|
|
729
762
|
const { line, col } = token.location;
|
|
730
763
|
if (art && (line > currLine || line === currLine && col > currCol))
|
|
731
764
|
return; // next doc comment after current token
|
|
@@ -750,6 +783,21 @@ class AstBuildingParser extends BaseParser {
|
|
|
750
783
|
}
|
|
751
784
|
}
|
|
752
785
|
|
|
786
|
+
checkWith( keyword ) {
|
|
787
|
+
if (this.lb() !== keyword)
|
|
788
|
+
return;
|
|
789
|
+
const tok = this.la();
|
|
790
|
+
if (this.docCommentIndex < tok.location.tokenIndex &&
|
|
791
|
+
this.docCommentIndex > this.lb().location.tokenIndex)
|
|
792
|
+
return;
|
|
793
|
+
const expecting = this.expectingArray(); // TODO: filter
|
|
794
|
+
const msg = this.warning( 'syntax-unexpected-semicolon', tok,
|
|
795
|
+
{ offending: antlrName( tok ), expecting, keyword: 'with' },
|
|
796
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
797
|
+
'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
|
|
798
|
+
msg.expectedTokens = expecting;
|
|
799
|
+
}
|
|
800
|
+
|
|
753
801
|
setNullability( art, val, location = this.lb().location ) {
|
|
754
802
|
const notNull = { val, location };
|
|
755
803
|
if (art.notNull) {
|
|
@@ -843,7 +891,20 @@ class AstBuildingParser extends BaseParser {
|
|
|
843
891
|
}
|
|
844
892
|
}
|
|
845
893
|
|
|
894
|
+
// TODO: remove the check from the parser; move it to shared.js
|
|
895
|
+
checkTypeArgs( art ) {
|
|
896
|
+
const args = art.$typeArgs;
|
|
897
|
+
// One or two arguments are interpreted as either length or precision/scale.
|
|
898
|
+
if (args.length > 2) {
|
|
899
|
+
const loc = args[2].location;
|
|
900
|
+
this.error( 'syntax-unexpected-argument', loc, {}, 'Too many type arguments' );
|
|
901
|
+
art.$typeArgs.length = 0;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
846
905
|
locationOfPrevTokens( offset ) {
|
|
906
|
+
// TODO: use combined location of lb() and la() and move actions accordingly
|
|
907
|
+
// (for error recovery)
|
|
847
908
|
const { file, line, col } = this.tokens[this.tokenIdx - offset].location;
|
|
848
909
|
const { endLine, endCol } = this.lb().location;
|
|
849
910
|
return {
|
|
@@ -912,7 +973,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
912
973
|
|
|
913
974
|
pushXprToken( expr ) {
|
|
914
975
|
const token = this.lb();
|
|
915
|
-
(expr.args ?? expr).push( {
|
|
976
|
+
(expr.args ?? expr).push?.( {
|
|
916
977
|
val: token.keyword ?? token.type,
|
|
917
978
|
location: token.location,
|
|
918
979
|
literal: 'token',
|
|
@@ -922,7 +983,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
922
983
|
applyOpToken( expr, nary = null ) {
|
|
923
984
|
const token = this.lb();
|
|
924
985
|
const op = { val: token.keyword ?? token.type, location: token.location, literal: 'token' };
|
|
925
|
-
if (nary === 'nary' && !expr
|
|
986
|
+
if (nary === 'nary' && expr && !expr.$parens) {
|
|
926
987
|
const { args } = expr;
|
|
927
988
|
const prev = args?.[1];
|
|
928
989
|
if (prev?.val === op.val && prev?.literal === 'token') {
|
|
@@ -944,7 +1005,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
944
1005
|
const { args, id, location } = path[0];
|
|
945
1006
|
if (args
|
|
946
1007
|
? path[0].$syntax === ':'
|
|
947
|
-
: path[0].$delimited || !
|
|
1008
|
+
: path[0].$delimited || !functionsWithoutParentheses.includes( id.toUpperCase() ))
|
|
948
1009
|
return this.attachLocation( ref );
|
|
949
1010
|
|
|
950
1011
|
const funcToken = this.prevTokenWithIndex( location.tokenIndex );
|
|
@@ -1013,11 +1074,11 @@ class AstBuildingParser extends BaseParser {
|
|
|
1013
1074
|
}
|
|
1014
1075
|
|
|
1015
1076
|
associationInSelectItem( art ) {
|
|
1016
|
-
|
|
1017
|
-
const path = value?.path;
|
|
1077
|
+
this.classifyImplicitName( 'ItemAssoc', art.value );
|
|
1078
|
+
const path = art.value?.path;
|
|
1018
1079
|
// we cannot compare "just one token before `:`" because there might be annos
|
|
1019
1080
|
if (path && path.length === 1 && !art.name && !art.expand && !art.inline) {
|
|
1020
|
-
const name =
|
|
1081
|
+
const name = path[0];
|
|
1021
1082
|
if (path.length === 1 && !name.args && !name.cardinality && !name.where) {
|
|
1022
1083
|
art.name = name;
|
|
1023
1084
|
delete art.value;
|
|
@@ -1130,6 +1191,9 @@ class AstBuildingParser extends BaseParser {
|
|
|
1130
1191
|
// - if `name` is an object, `name.id` is either set, or the (local) name is calculated
|
|
1131
1192
|
// from the IDs of all items in `name.path` (for main artifact definitions).
|
|
1132
1193
|
addDef( art, parent, env, kind, name ) {
|
|
1194
|
+
if (!art)
|
|
1195
|
+
return art; // parser error
|
|
1196
|
+
|
|
1133
1197
|
if (Array.isArray(name)) {
|
|
1134
1198
|
const last = name.length && name[name.length - 1];
|
|
1135
1199
|
art.name = { // A.B.C -> 'C'
|