@sap/cds-compiler 5.6.0 → 5.7.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 +38 -0
- package/bin/cdsse.js +1 -0
- package/bin/cdsv2m.js +2 -1
- package/doc/Versioning.md +4 -4
- package/lib/api/options.js +1 -0
- package/lib/base/builtins.js +2 -2
- package/lib/base/dictionaries.js +1 -2
- package/lib/base/keywords.js +3 -1
- package/lib/base/lazyload.js +1 -1
- package/lib/base/message-registry.js +169 -144
- package/lib/base/messages.js +69 -59
- package/lib/base/model.js +3 -3
- package/lib/base/node-helpers.js +17 -16
- package/lib/base/optionProcessorHelper.js +13 -14
- package/lib/base/shuffle.js +4 -1
- package/lib/checks/structuredAnnoExpressions.js +1 -1
- package/lib/compiler/assert-consistency.js +1 -1
- package/lib/compiler/builtins.js +2 -1
- package/lib/compiler/extend.js +20 -5
- package/lib/compiler/resolve.js +45 -9
- package/lib/compiler/shared.js +1 -0
- package/lib/edm/annotations/edmJson.js +3 -3
- package/lib/edm/annotations/genericTranslation.js +5 -1
- package/lib/edm/annotations/vocabularyDefinitions.js +2 -2
- package/lib/edm/edmUtils.js +2 -1
- package/lib/gen/BaseParser.js +32 -32
- package/lib/gen/CdlParser.js +1526 -1488
- package/lib/json/from-csn.js +2 -0
- package/lib/json/to-csn.js +13 -4
- package/lib/language/docCommentParser.js +11 -5
- package/lib/language/errorStrategy.js +3 -3
- package/lib/language/genericAntlrParser.js +2 -0
- package/lib/model/csnUtils.js +6 -1
- package/lib/optionProcessor.js +5 -1
- package/lib/parsers/AstBuildingParser.js +161 -73
- package/lib/parsers/CdlGrammar.g4 +129 -85
- package/lib/parsers/Lexer.js +5 -3
- package/lib/parsers/index.js +1 -1
- package/lib/render/toCdl.js +6 -5
- package/lib/render/toHdbcds.js +1 -1
- package/lib/render/toSql.js +5 -3
- package/lib/render/utils/common.js +19 -6
- package/lib/render/utils/delta.js +1 -3
- package/lib/render/utils/standardDatabaseFunctions.js +576 -0
- package/lib/transform/addTenantFields.js +2 -1
- package/lib/transform/db/flattening.js +18 -77
- package/lib/transform/db/groupByOrderBy.js +2 -2
- package/lib/transform/db/rewriteCalculatedElements.js +14 -19
- package/lib/transform/db/temporal.js +2 -1
- package/lib/transform/odata/adaptAnnotationRefs.js +79 -0
- package/lib/transform/odata/createForeignKeys.js +4 -71
- package/lib/transform/odata/flattening.js +11 -1
- package/lib/transform/transformUtils.js +20 -85
- package/package.json +2 -1
- package/bin/cds_update_annotations.js +0 -180
package/lib/json/from-csn.js
CHANGED
|
@@ -486,6 +486,7 @@ const schema = compileSchema( {
|
|
|
486
486
|
val: {
|
|
487
487
|
type: value,
|
|
488
488
|
inKind: [ '$column', 'enum' ],
|
|
489
|
+
xorException: '#', // see xorGroup :expr
|
|
489
490
|
// see also extra handling for 'element' in extension, see definition()
|
|
490
491
|
},
|
|
491
492
|
literal: {
|
|
@@ -499,6 +500,7 @@ const schema = compileSchema( {
|
|
|
499
500
|
// Note: We emit a warning if '#' is used in enums. Because the compiler
|
|
500
501
|
// can generate CSN like this, we need to be able to parse it.
|
|
501
502
|
inKind: [ '$column', 'enum' ],
|
|
503
|
+
xorException: 'val', // see xorGroup :expr
|
|
502
504
|
// see also extra handling for 'element' in extension, see definition()
|
|
503
505
|
},
|
|
504
506
|
path: { // in CSN v0.1.0 'foreignKeys'
|
package/lib/json/to-csn.js
CHANGED
|
@@ -1132,7 +1132,7 @@ function value( node ) {
|
|
|
1132
1132
|
return extra( { '=': node.variant ? `${ ref }#${ pathName(node.variant.path) }` : ref }, node );
|
|
1133
1133
|
}
|
|
1134
1134
|
if (node.literal === 'enum')
|
|
1135
|
-
return
|
|
1135
|
+
return enumValue( node );
|
|
1136
1136
|
if (node.literal === 'array')
|
|
1137
1137
|
return node.val.map( value );
|
|
1138
1138
|
if (node.literal === 'token' && node.val === '...')
|
|
@@ -1146,6 +1146,15 @@ function value( node ) {
|
|
|
1146
1146
|
return r;
|
|
1147
1147
|
}
|
|
1148
1148
|
|
|
1149
|
+
function enumValue( node ) {
|
|
1150
|
+
const r = extra( { '#': node.sym.id }, node );
|
|
1151
|
+
const sym = node.sym._artifact;
|
|
1152
|
+
// add calculated `val`, but not for chained symbols:
|
|
1153
|
+
if (sym && (!gensrcFlavor || gensrcFlavor === 'column') && !sym.value?.sym)
|
|
1154
|
+
r.val = sym.value ? sym.value.val : sym.name.id;
|
|
1155
|
+
return r;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1149
1158
|
function targetElement( val, csn, node ) {
|
|
1150
1159
|
const key = addExplicitAs( { ref: val.path.map( pathItem ) },
|
|
1151
1160
|
node.name, neqPath( val ) );
|
|
@@ -1210,10 +1219,10 @@ function exprInternal( node, xprParens ) {
|
|
|
1210
1219
|
return extra( { ref }, node );
|
|
1211
1220
|
}
|
|
1212
1221
|
if (node.literal) {
|
|
1213
|
-
if (
|
|
1222
|
+
if (node.literal === 'enum')
|
|
1223
|
+
return enumValue( node );
|
|
1224
|
+
else if (typeof node.val === node.literal || node.val === null)
|
|
1214
1225
|
return extra( { val: node.val }, node );
|
|
1215
|
-
else if (node.literal === 'enum')
|
|
1216
|
-
return extra( { '#': node.sym.id }, node );
|
|
1217
1226
|
else if (node.literal === 'token')
|
|
1218
1227
|
return node.val; // * in COUNT(*)
|
|
1219
1228
|
return extra( { val: node.val, literal: node.literal }, node );
|
|
@@ -7,7 +7,6 @@ const {
|
|
|
7
7
|
} = require('./textUtils');
|
|
8
8
|
|
|
9
9
|
const fencedCommentRegEx = /^\s*[*]/;
|
|
10
|
-
const footerFenceRegEx = /\s*[*]+\/$/;
|
|
11
10
|
const hasContentOnFirstLineRegEx = /\/\*+\s*\S/;
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -37,9 +36,8 @@ function parseDocComment( comment ) {
|
|
|
37
36
|
if (lines.length === 1) {
|
|
38
37
|
// Special case for one-liners.
|
|
39
38
|
// Remove "/***/" and trim white space and asterisks.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.replace(/\**\/$/, '') // for `/*****/`, only `/` remains
|
|
39
|
+
let content = lines[0].replace(/^\/[*]{2,}/, '');
|
|
40
|
+
content = removeFooterFence(content) // for `/*****/`, only `/` remains
|
|
43
41
|
.replace('*\\/', '*/') // escape sequence
|
|
44
42
|
.trim();
|
|
45
43
|
return isWhitespaceOrNewLineOnly(content) ? null : content;
|
|
@@ -153,7 +151,15 @@ function removeHeaderFence( line ) {
|
|
|
153
151
|
* @returns {string} header without fence
|
|
154
152
|
*/
|
|
155
153
|
function removeFooterFence( line ) {
|
|
156
|
-
|
|
154
|
+
let trimAt = line.length - 1;
|
|
155
|
+
// '-1': remove trailing `/`
|
|
156
|
+
for (let i = trimAt - 1; i >= 0 && line[i] === '*'; --i)
|
|
157
|
+
trimAt = i;
|
|
158
|
+
// We know that trimAt is at a '*', regardless of whether the previous loop ran.
|
|
159
|
+
for (let i = trimAt - 1; i >= 0 && /^\s$/.test(line[i]); --i)
|
|
160
|
+
trimAt = i;
|
|
161
|
+
// Either trimAt is a ' ' or '*', regardless of whether any loop ran.
|
|
162
|
+
return line.slice(0, trimAt);
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
/**
|
|
@@ -235,9 +235,9 @@ function singleTokenDeletion( recognizer ) {
|
|
|
235
235
|
return null;
|
|
236
236
|
|
|
237
237
|
const nextTokenType = recognizer.getTokenStream().LA(2);
|
|
238
|
-
const { Number } = recognizer.constructor;
|
|
239
|
-
if (nextTokenType >
|
|
240
|
-
token.type <=
|
|
238
|
+
const { Number: num } = recognizer.constructor;
|
|
239
|
+
if (nextTokenType > num && // next token is Id|Unreserved|IllegalToken
|
|
240
|
+
token.type <= num) // current token is not
|
|
241
241
|
return null;
|
|
242
242
|
|
|
243
243
|
const expecting = this.getExpectedTokens(recognizer);
|
|
@@ -1059,6 +1059,8 @@ function assignAnnotationValue( anno, value ) {
|
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
1061
|
function relevantDigits( val ) {
|
|
1062
|
+
// We know the value does not contain newlines, hence the RegEx is safe.
|
|
1063
|
+
// eslint-disable-next-line sonarjs/slow-regex
|
|
1062
1064
|
val = val.replace( /e.+$/i, '' );
|
|
1063
1065
|
|
|
1064
1066
|
// To avoid the super-linear RegEx `0+$`, use the non-backtracking version and
|
package/lib/model/csnUtils.js
CHANGED
|
@@ -1014,7 +1014,12 @@ function getRootArtifactName( artifactName, csn ) {
|
|
|
1014
1014
|
// 'foo' => 'foo';
|
|
1015
1015
|
// 'foo::bar' => 'bar'
|
|
1016
1016
|
function getLastPartOf( name ) {
|
|
1017
|
-
|
|
1017
|
+
// Not using RegEx /[^.:]+$/ to avoid ReDoS.
|
|
1018
|
+
for (let i = name.length - 1; i >= 0; --i) {
|
|
1019
|
+
if (name[i] === '.' || name[i] === ':')
|
|
1020
|
+
return name.substring(i + 1);
|
|
1021
|
+
}
|
|
1022
|
+
return name;
|
|
1018
1023
|
}
|
|
1019
1024
|
|
|
1020
1025
|
// Return the last part of reference array 'ref'
|
package/lib/optionProcessor.js
CHANGED
|
@@ -197,6 +197,7 @@ optionProcessor.command('H, toHana')
|
|
|
197
197
|
.option(' --assert-integrity-type <type>', { valid: ['RT', 'DB'], ignoreCase: true })
|
|
198
198
|
.option(' --pre2134ReferentialConstraintNames')
|
|
199
199
|
.option(' --disable-hana-comments')
|
|
200
|
+
.option(' --standard-database-functions')
|
|
200
201
|
.help(`
|
|
201
202
|
Usage: cdsc toHana [options] <files...>
|
|
202
203
|
|
|
@@ -233,6 +234,7 @@ optionProcessor.command('H, toHana')
|
|
|
233
234
|
DB : Create database constraints for associations
|
|
234
235
|
--pre2134ReferentialConstraintNames Do not prefix the constraint identifier with "c__"
|
|
235
236
|
--disable-hana-comments Disable rendering of doc comments as SAP HANA comments.
|
|
237
|
+
--standard-database-functions Enable rendering of standard database function mappings.
|
|
236
238
|
`);
|
|
237
239
|
|
|
238
240
|
optionProcessor.command('O, toOdata')
|
|
@@ -340,6 +342,7 @@ optionProcessor.command('Q, toSql')
|
|
|
340
342
|
.option(' --better-sqlite-session-variables <bool>')
|
|
341
343
|
.option(' --transitive-localized-views')
|
|
342
344
|
.option(' --with-hana-associations <bool>', { valid: [ 'true', 'false' ] })
|
|
345
|
+
.option(' --standard-database-functions')
|
|
343
346
|
.help(`
|
|
344
347
|
Usage: cdsc toSql [options] <files...>
|
|
345
348
|
|
|
@@ -399,9 +402,10 @@ optionProcessor.command('Q, toSql')
|
|
|
399
402
|
--transitive-localized-views If set, the backends will create localized convenience views for
|
|
400
403
|
those views, that only have an association to a localized entity/view.
|
|
401
404
|
--with-hana-associations <bool>
|
|
402
|
-
Enable
|
|
405
|
+
Enable or disable rendering of "WITH ASSOCIATIONS" for sqlDialect 'hana'.
|
|
403
406
|
true : (default) Render "WITH ASSOCIATIONS"
|
|
404
407
|
false : Do not render "WITH ASSOCIATIONS"
|
|
408
|
+
--standard-database-functions Enable rendering of standard database function mappings.
|
|
405
409
|
`);
|
|
406
410
|
|
|
407
411
|
optionProcessor.command('toRename')
|
|
@@ -9,6 +9,7 @@ const { functionsWithoutParentheses } = require('./identifiers');
|
|
|
9
9
|
const { pathName } = require('../compiler/utils');
|
|
10
10
|
const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
|
|
11
11
|
const parserTokens = { // TODO: precompile into specialFunction
|
|
12
|
+
__proto__: null,
|
|
12
13
|
GenericIntro: 'intro',
|
|
13
14
|
GenericExpr: 'expr',
|
|
14
15
|
GenericSeparator: 'separator',
|
|
@@ -36,11 +37,18 @@ const queryOps = {
|
|
|
36
37
|
minus: 'query',
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
const extensionsCode = {
|
|
41
|
+
__proto__: null,
|
|
42
|
+
definitions: 'extend … with definitions',
|
|
43
|
+
context: 'extend context',
|
|
44
|
+
service: 'extend service',
|
|
45
|
+
};
|
|
46
|
+
|
|
39
47
|
const PRECEDENCE_OF_IN_PREDICATE = 10;
|
|
40
48
|
const PRECEDENCE_OF_EQUAL = 10;
|
|
41
49
|
|
|
42
50
|
class AstBuildingParser extends BaseParser {
|
|
43
|
-
leanConditions = { afterBrace: true };
|
|
51
|
+
leanConditions = { afterBrace: true, fail: true };
|
|
44
52
|
|
|
45
53
|
constructor( lexer, keywords, table, options, messageFunctions ) {
|
|
46
54
|
super( lexer, keywords, table ); // lexer has file
|
|
@@ -86,12 +94,18 @@ class AstBuildingParser extends BaseParser {
|
|
|
86
94
|
|
|
87
95
|
reportUnexpectedToken_() {
|
|
88
96
|
const token = this.la();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
97
|
+
const args = { offending: this.antlrName( token ), expecting: this.expectingArray() };
|
|
98
|
+
const errorMethod = this.conditionTokenIdx === this.tokenIdx &&
|
|
99
|
+
this[`${ this.conditionName }Error`];
|
|
100
|
+
let err = errorMethod && errorMethod.call( this, args, token );
|
|
101
|
+
// TODO: should we set the msg variant always? (→ no nestedExpandError necessary)
|
|
102
|
+
if (errorMethod && !err)
|
|
103
|
+
args['#'] ??= this.conditionName;
|
|
104
|
+
err ||= this.error( 'syntax-unexpected-token', token, args );
|
|
92
105
|
// No 'unwanted' variant, no 'syntax-missing-token'
|
|
93
|
-
err.expectedTokens = expecting;
|
|
106
|
+
err.expectedTokens = args.expecting;
|
|
94
107
|
}
|
|
108
|
+
|
|
95
109
|
reportReservedWord_() {
|
|
96
110
|
const token = this.la();
|
|
97
111
|
const err = this.message( 'syntax-unexpected-reserved-word', token,
|
|
@@ -100,13 +114,13 @@ class AstBuildingParser extends BaseParser {
|
|
|
100
114
|
err.expectedTokens = this.expectingArray();
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
tableWithoutAs() {
|
|
117
|
+
tableWithoutAs() { // not used in <guard=…>, only called by other guard
|
|
104
118
|
// TODO TOOL: if the tool properly creates `default: this.giR()`, this
|
|
105
119
|
// condition method is most likely not necessary
|
|
106
120
|
const { keyword } = this.la();
|
|
107
121
|
// TODO: if necessary, we could allow some keywords, and just make sure that
|
|
108
122
|
// all JOIN variants are still possible
|
|
109
|
-
return
|
|
123
|
+
return keyword && this.keywords[keyword] != null;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/**
|
|
@@ -116,22 +130,22 @@ class AstBuildingParser extends BaseParser {
|
|
|
116
130
|
* recursive call, it can finally turn to be both a query or expr/table
|
|
117
131
|
* - <prepare=queryOnLeft, arg=‹SomeVal›>: make the current parentheses
|
|
118
132
|
* context to be not a query anymore
|
|
119
|
-
* - <
|
|
120
|
-
* - <
|
|
133
|
+
* - <guard=queryOnLeft> tests whether the expression on the left is a query
|
|
134
|
+
* - <guard=queryOnLeft, arg=‹SomeVal›>: tests whether the expression on the
|
|
121
135
|
* left is a query, then make the current context to be not a query anymore
|
|
122
|
-
* - <
|
|
136
|
+
* - <guard=queryOnLeft, arg=tableWithoutAs>: …after having checked
|
|
123
137
|
* whether the next token is no (reserved or unreserved) keyword
|
|
124
138
|
*/
|
|
125
139
|
queryOnLeft( test, arg ) {
|
|
126
140
|
if (arg === 'tableWithoutAs') {
|
|
127
|
-
if (
|
|
128
|
-
return
|
|
141
|
+
if (this.tableWithoutAs())
|
|
142
|
+
return true;
|
|
129
143
|
}
|
|
130
144
|
else if (!arg && !test) {
|
|
131
145
|
// provide new dynamic parentheses context, except with direct
|
|
132
146
|
// recursive call:
|
|
133
147
|
if (this.inSameRule_( this.s, this.stack.at( -1 ).followState ))
|
|
134
|
-
return
|
|
148
|
+
return false;
|
|
135
149
|
this.dynamic_.parenthesesCtx = [ null ];
|
|
136
150
|
this._tracePush( 'Parentheses()' );
|
|
137
151
|
}
|
|
@@ -139,7 +153,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
139
153
|
const noQuery = parenthesesCtx?.[0];
|
|
140
154
|
if (arg && parenthesesCtx)
|
|
141
155
|
parenthesesCtx[0] = arg;
|
|
142
|
-
return
|
|
156
|
+
return noQuery;
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
prepareSpecialFunction() {
|
|
@@ -187,14 +201,21 @@ class AstBuildingParser extends BaseParser {
|
|
|
187
201
|
return (generic === 'separator') ? 'GenericSeparator' : ',';
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
|
|
191
|
-
const realTokens = this.dynamic_.generic?.[parserTokens[tokenName]];
|
|
192
|
-
// TODO: avoid parserTokens dict, use lower-case in specialFunctions
|
|
193
|
-
|
|
204
|
+
addTokenToSet_( set, tokenName, val, collectKeywordsOnly ) {
|
|
205
|
+
const realTokens = parserTokens[tokenName] && this.dynamic_.generic?.[parserTokens[tokenName]];
|
|
206
|
+
// TODO: avoid 2nd parserTokens dict use, use lower-case in specialFunctions
|
|
207
|
+
if (!realTokens) {
|
|
208
|
+
super.addTokenToSet_( set, tokenName, val, collectKeywordsOnly );
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
for (const t of realTokens)
|
|
212
|
+
super.addTokenToSet_( set, t.toLowerCase(), val, collectKeywordsOnly );
|
|
213
|
+
}
|
|
194
214
|
}
|
|
195
215
|
|
|
196
216
|
inSelectItem( _test, arg ) { // only as action
|
|
197
|
-
this.dynamic_.inSelectItem = arg
|
|
217
|
+
this.dynamic_.inSelectItem = arg ||
|
|
218
|
+
(this.tokens[this.tokenIdx - 2].type === '.' ? 'inline' : 'expand');
|
|
198
219
|
}
|
|
199
220
|
|
|
200
221
|
/**
|
|
@@ -202,38 +223,47 @@ class AstBuildingParser extends BaseParser {
|
|
|
202
223
|
* (also inside sub queries in those, which will be rejected later anyway)
|
|
203
224
|
*/
|
|
204
225
|
modifierRestriction() {
|
|
205
|
-
|
|
226
|
+
const { inSelectItem } = this.dynamic_;
|
|
227
|
+
// TODO: really reject for top-level "inline"?
|
|
228
|
+
return inSelectItem === 'expand' || inSelectItem === 'inline';
|
|
229
|
+
}
|
|
230
|
+
modifierRestrictionError( args, offending ) {
|
|
231
|
+
return this.error( 'syntax-unexpected-modifier', offending, args,
|
|
232
|
+
// TODO: we would have text variant for expand or inline,
|
|
233
|
+
// but we probably allow `key` in nested top-level inline
|
|
234
|
+
'Unexpected $(OFFENDING) in nested expand/inline, expecting $(EXPECTING)' );
|
|
206
235
|
}
|
|
207
236
|
|
|
208
237
|
isDotForPath() { // see also inSelectItem
|
|
238
|
+
// TODO: also consider whether we are in the <prefer>ed `valuePath` branch
|
|
209
239
|
if (this.dynamic_.inSelectItem == null)
|
|
210
|
-
return
|
|
240
|
+
return false;
|
|
211
241
|
// TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
|
|
212
242
|
// (as <prepare>)
|
|
213
243
|
const next = this.tokens[this.tokenIdx + 1]?.type;
|
|
214
|
-
return next
|
|
244
|
+
return next === '*' || next === '{';
|
|
215
245
|
}
|
|
216
246
|
|
|
217
|
-
notAfterEntityArgOrFilter( mode ) {
|
|
247
|
+
notAfterEntityArgOrFilter( mode ) { // TODO: for <hide>
|
|
218
248
|
if (mode !== 'M')
|
|
219
|
-
return
|
|
249
|
+
return false;
|
|
220
250
|
const { type } = this.lb();
|
|
221
251
|
if (type !== ')' && type !== ']')
|
|
222
|
-
return
|
|
252
|
+
return false;
|
|
223
253
|
const { followState } = this.stack.at( -1 );
|
|
224
|
-
return
|
|
254
|
+
return this.table[followState][':'];
|
|
225
255
|
}
|
|
226
256
|
|
|
227
257
|
// <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
|
|
228
258
|
// completion for `… default 3 not ~;` → currently just `null` but hey
|
|
229
259
|
isNegatedRelation( _test, prec ) {
|
|
230
|
-
return this.tokens[this.tokenIdx + 1]?.keyword
|
|
260
|
+
return this.tokens[this.tokenIdx + 1]?.keyword === 'null' ||
|
|
231
261
|
this.precNone_( _test, prec );
|
|
232
262
|
}
|
|
233
263
|
|
|
234
264
|
isNamedArg() {
|
|
235
|
-
const
|
|
236
|
-
return type
|
|
265
|
+
const type = this.tokens[this.tokenIdx + 1]?.type;
|
|
266
|
+
return type !== ':' && type !== '=>';
|
|
237
267
|
}
|
|
238
268
|
|
|
239
269
|
/**
|
|
@@ -241,45 +271,66 @@ class AstBuildingParser extends BaseParser {
|
|
|
241
271
|
* `namespace`
|
|
242
272
|
*/
|
|
243
273
|
namespaceRestriction() {
|
|
244
|
-
return ++this.topLevel$
|
|
274
|
+
return ++this.topLevel$ > 0;
|
|
245
275
|
}
|
|
246
276
|
|
|
247
277
|
/**
|
|
248
278
|
* `extend`/`annotate` is forbidden inside `extend … with definitions` and
|
|
249
|
-
* variants
|
|
279
|
+
* variants. TODO: combine with `vocabularyRestriction`.
|
|
250
280
|
*/
|
|
251
281
|
extensionRestriction() {
|
|
252
|
-
//
|
|
282
|
+
// 'syntax-unexpected-extension': 'Unexpected $(KEYWORD) inside $(CODE) block',
|
|
253
283
|
const r = this.dynamic_.inExtension;
|
|
254
|
-
this.dynamic_.inExtension =
|
|
255
|
-
return
|
|
284
|
+
this.dynamic_.inExtension = this.tokenIdx + 1;
|
|
285
|
+
return r;
|
|
286
|
+
}
|
|
287
|
+
extensionRestrictionError( args, token ) {
|
|
288
|
+
const extendIdx = this.conditionFailure;
|
|
289
|
+
const variant = this.tokens[extendIdx + 1]?.type === 'Id' &&
|
|
290
|
+
this.tokens[extendIdx].keyword;
|
|
291
|
+
args.code = extensionsCode[variant] || extensionsCode.definitions;
|
|
292
|
+
args['#'] = 'new-parser';
|
|
293
|
+
return this.error( 'syntax-unexpected-extension', token, args );
|
|
256
294
|
}
|
|
257
295
|
|
|
258
296
|
/**
|
|
259
|
-
* `annotation` def is only allowed top-level
|
|
297
|
+
* `annotation` def is only allowed top-level. TODO: combine with `extensionRestriction`
|
|
260
298
|
*/
|
|
261
299
|
vocabularyRestriction( test ) {
|
|
262
|
-
// TODO: use `syntax-unexpected-vocabulary` as message
|
|
263
300
|
if (!test)
|
|
264
|
-
this.dynamic_.inBlock =
|
|
265
|
-
return
|
|
301
|
+
this.dynamic_.inBlock = this.tokenIdx;
|
|
302
|
+
return this.dynamic_.inBlock ?? this.dynamic_.inExtension;
|
|
303
|
+
}
|
|
304
|
+
vocabularyRestrictionError( args, token ) {
|
|
305
|
+
const extendIdx = this.conditionFailure;
|
|
306
|
+
args['#'] = `${ this.tokens[extendIdx - 1].keyword }-new`;
|
|
307
|
+
return this.error( 'syntax-unexpected-vocabulary', token, args );
|
|
266
308
|
}
|
|
267
309
|
|
|
268
310
|
/**
|
|
269
311
|
* Restrictions according to the expression of a select column.
|
|
270
312
|
* Currently only to restrict it to a single `Id` for published associations.
|
|
313
|
+
* No extra syntax-unexpected-assoc for failure.
|
|
271
314
|
*/
|
|
272
315
|
columnExpr( mode, arg ) {
|
|
273
316
|
if (mode)
|
|
274
|
-
return this.columnExpr$;
|
|
275
|
-
// TODO: should we use (text of) syntax-unexpected-assoc somewhere ?
|
|
317
|
+
return !this.columnExpr$;
|
|
276
318
|
if (arg)
|
|
277
319
|
this.columnExpr$ = this.tokenIdx;
|
|
278
320
|
else if (this.columnExpr$ !== this.tokenIdx - 1 ||
|
|
279
321
|
this.lb().type !== 'Id' ||
|
|
280
322
|
[ 'true', 'false', 'null' ].includes( this.lb().keyword ) )
|
|
281
323
|
this.columnExpr$ = null;
|
|
282
|
-
return
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
nestedExpand( mode ) {
|
|
328
|
+
if (!mode)
|
|
329
|
+
this.nestedExpand$ = this.tokenIdx;
|
|
330
|
+
return this.nestedExpand$ !== this.tokenIdx;
|
|
331
|
+
}
|
|
332
|
+
nestedExpandError() {
|
|
333
|
+
// This is intentionally left empty
|
|
283
334
|
}
|
|
284
335
|
|
|
285
336
|
/**
|
|
@@ -299,7 +350,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
299
350
|
* now disallow annotation assignments after `= calcExpr`,
|
|
300
351
|
* ignore doc comment after having called `typeExpression`
|
|
301
352
|
*
|
|
302
|
-
* Called as <
|
|
353
|
+
* Called as <guard=…>:
|
|
303
354
|
*
|
|
304
355
|
* - <…, arg=default> in `typeExpression` and `typeProperties`
|
|
305
356
|
* is `default` allowed? If used, disallow calc and further DEFAULT
|
|
@@ -321,14 +372,14 @@ class AstBuildingParser extends BaseParser {
|
|
|
321
372
|
let { elementCtx } = this.dynamic_;
|
|
322
373
|
if (test) {
|
|
323
374
|
if (elementCtx?.[0] === arg)
|
|
324
|
-
return
|
|
375
|
+
return arg;
|
|
325
376
|
if (!elementCtx) { // with type, param, or annotation defs
|
|
326
377
|
// eslint-disable-next-line no-multi-assign
|
|
327
378
|
elementCtx = this.dynamic_.elementCtx = [ null, false, false ];
|
|
328
379
|
}
|
|
329
380
|
if (arg === 'default') {
|
|
330
381
|
if (elementCtx[1])
|
|
331
|
-
return
|
|
382
|
+
return true;
|
|
332
383
|
elementCtx[1] = true;
|
|
333
384
|
elementCtx[0] = 'calc';
|
|
334
385
|
this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
|
|
@@ -336,7 +387,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
336
387
|
else if (arg === 'notNull') {
|
|
337
388
|
if (elementCtx[2]) {
|
|
338
389
|
if (this.la().keyword !== elementCtx[2] || test === 'M') // TODO v6: always error
|
|
339
|
-
return
|
|
390
|
+
return true; // error if different nullibility specification
|
|
340
391
|
}
|
|
341
392
|
elementCtx[2] = this.la().keyword;
|
|
342
393
|
}
|
|
@@ -347,17 +398,34 @@ class AstBuildingParser extends BaseParser {
|
|
|
347
398
|
else if (elementCtx) {
|
|
348
399
|
elementCtx[0] = arg;
|
|
349
400
|
}
|
|
350
|
-
return
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
elementRestrictionError( args, token ) {
|
|
404
|
+
if (this.conditionFailure !== 'calc')
|
|
405
|
+
return null;
|
|
406
|
+
args.keyword = 'default';
|
|
407
|
+
// TODO: investigate why 'null', '@' are not in the expected-set
|
|
408
|
+
// TODO: simplified version for predictions, such that ops are in expected-set ?
|
|
409
|
+
// TODO: also test `default 3 null = 4`
|
|
410
|
+
return this.error( 'syntax-unexpected-calc', token, args,
|
|
411
|
+
'Unexpected $(OFFENDING) after $(KEYWORD) clause, expecting $(EXPECTING)' );
|
|
351
412
|
}
|
|
352
413
|
|
|
353
414
|
noRepeatedCardinality( mode ) {
|
|
354
415
|
if (this.tokens[this.tokenIdx - 2]?.type !== ']')
|
|
355
|
-
return true;
|
|
356
|
-
if (mode === 'M')
|
|
357
416
|
return false;
|
|
417
|
+
if (mode === 'M')
|
|
418
|
+
return true;
|
|
358
419
|
// currently just warning if same cardinality provided twice
|
|
359
420
|
const same = { one: '1', many: '*' }[this.la().keyword];
|
|
360
|
-
return this.tokens[this.tokenIdx - 3]?.text
|
|
421
|
+
return this.tokens[this.tokenIdx - 3]?.text !== same;
|
|
422
|
+
}
|
|
423
|
+
noRepeatedCardinalityError( args ) {
|
|
424
|
+
let openIdx = this.tokenIdx - 2;
|
|
425
|
+
while (this.tokens[--openIdx].type !== '[')
|
|
426
|
+
;
|
|
427
|
+
args.location = this.tokens[openIdx].location;
|
|
428
|
+
args.code = '[…]';
|
|
361
429
|
}
|
|
362
430
|
|
|
363
431
|
/**
|
|
@@ -366,21 +434,25 @@ class AstBuildingParser extends BaseParser {
|
|
|
366
434
|
*
|
|
367
435
|
* Beware: mentioned in leanConditions, i.e. executed in predictions!
|
|
368
436
|
*/
|
|
369
|
-
afterBrace( test ) {
|
|
370
|
-
if (!test)
|
|
371
|
-
this.afterBrace$
|
|
437
|
+
afterBrace( test, arg ) {
|
|
438
|
+
if (!test) {
|
|
439
|
+
this.afterBrace$
|
|
440
|
+
= (!arg || this.afterBrace$ > 0 && this.tokens[this.afterBrace$ - 1].keyword === arg)
|
|
441
|
+
? this.tokenIdx
|
|
442
|
+
: -1;
|
|
443
|
+
}
|
|
372
444
|
// TODO TOOL: the following test belongs to the BaseParser.js:
|
|
373
445
|
if (this.conditionTokenIdx === this.tokenIdx && // tested on same
|
|
374
446
|
this.conditionStackLength == null && // after error recover
|
|
375
447
|
test !== 'M')
|
|
376
|
-
return
|
|
448
|
+
return false;
|
|
377
449
|
// Strange optional `;` after PROJECTION ON source: the rule exit prediction
|
|
378
450
|
// for fromRefWithOptAlias etc now checks C(afterBrace):
|
|
379
451
|
if (test === 'E' && this.afterBrace$ > 0 &&
|
|
380
452
|
this.tokens[this.afterBrace$ - 1]?.keyword === 'projection' &&
|
|
381
453
|
this.tokens[this.afterBrace$].keyword === 'on')
|
|
382
|
-
return
|
|
383
|
-
return this.afterBrace$
|
|
454
|
+
return false;
|
|
455
|
+
return this.afterBrace$ !== this.tokenIdx;
|
|
384
456
|
}
|
|
385
457
|
|
|
386
458
|
/**
|
|
@@ -389,8 +461,8 @@ class AstBuildingParser extends BaseParser {
|
|
|
389
461
|
annoInSameLine( test ) {
|
|
390
462
|
if (!test)
|
|
391
463
|
this.dynamic_.safeAnno = true;
|
|
392
|
-
return this.dynamic_.safeAnno
|
|
393
|
-
this.lb().location.line
|
|
464
|
+
return !this.dynamic_.safeAnno &&
|
|
465
|
+
this.lb().location.line !== this.la().location.line;
|
|
394
466
|
}
|
|
395
467
|
|
|
396
468
|
/**
|
|
@@ -405,21 +477,39 @@ class AstBuildingParser extends BaseParser {
|
|
|
405
477
|
else if (arg === 'ellipsis') { // on '...'
|
|
406
478
|
const { arrayAnno } = this.dynamic_;
|
|
407
479
|
if (!arrayAnno[0])
|
|
408
|
-
return
|
|
480
|
+
return arrayAnno[0] == null ? 'duplicate' : arg;
|
|
409
481
|
arrayAnno[0] = this.tokens[this.tokenIdx + 1]?.keyword;
|
|
410
482
|
}
|
|
411
|
-
else if (arg === 'bracket') {
|
|
483
|
+
else if (arg === 'bracket') { // syntax-invalid-ellipsis
|
|
412
484
|
// closing bracket not allowed if last `...` in array is with `up to
|
|
413
|
-
return typeof this.dynamic_.arrayAnno[0]
|
|
485
|
+
return typeof this.dynamic_.arrayAnno[0] === 'string' && arg;
|
|
414
486
|
}
|
|
415
|
-
else { // orNotEmpty
|
|
416
|
-
return this.dynamic_.arrayAnno
|
|
487
|
+
else { // orNotEmpty -> anno value must not be empty struct
|
|
488
|
+
return !this.dynamic_.arrayAnno && this.lb().type === '{' && 'empty';
|
|
417
489
|
}
|
|
418
|
-
return
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
arrayAnnoError( args, token ) {
|
|
493
|
+
if (this.conditionFailure === 'duplicate') {
|
|
494
|
+
args['#'] = 'std'; // normal syntax-unexpected-token
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
args.code = '...';
|
|
498
|
+
args['#'] = this.conditionFailure;
|
|
499
|
+
return this.error( 'syntax-invalid-anno', token, args );
|
|
419
500
|
}
|
|
420
501
|
|
|
421
502
|
beforeColon() {
|
|
422
|
-
return this.tokens[this.tokenIdx + 1]?.text
|
|
503
|
+
return this.tokens[this.tokenIdx + 1]?.text !== ':';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fail( mode ) {
|
|
507
|
+
// TODO TOOL: the following test belongs to the BaseParser.js:
|
|
508
|
+
if (this.conditionTokenIdx === this.tokenIdx && // tested on same
|
|
509
|
+
this.conditionStackLength == null && // after error recover
|
|
510
|
+
mode !== 'M')
|
|
511
|
+
return false;
|
|
512
|
+
return true; // mode !== 'Y';
|
|
423
513
|
}
|
|
424
514
|
|
|
425
515
|
// Space handling etc, locations ----------------------------------------------
|
|
@@ -635,12 +725,9 @@ class AstBuildingParser extends BaseParser {
|
|
|
635
725
|
const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
|
|
636
726
|
const token = this.prevTokenWithIndex( tokenIndex ) ?? this.tokens[this.tokenIdx - 1];
|
|
637
727
|
const { parsedAs } = token;
|
|
638
|
-
if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword')
|
|
728
|
+
if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword')
|
|
639
729
|
token.parsedAs = category;
|
|
640
|
-
return { token, parsedAs };
|
|
641
|
-
}
|
|
642
730
|
}
|
|
643
|
-
return null;
|
|
644
731
|
}
|
|
645
732
|
|
|
646
733
|
taggedIfQuery( query ) {
|
|
@@ -898,7 +985,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
898
985
|
return target;
|
|
899
986
|
}
|
|
900
987
|
|
|
901
|
-
//
|
|
988
|
+
// see also <guard=nestedExpand>
|
|
902
989
|
reportExpandInline( column, isInline ) {
|
|
903
990
|
// called before matching `{`
|
|
904
991
|
if (column.value && !column.value.path) {
|
|
@@ -1129,7 +1216,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
1129
1216
|
};
|
|
1130
1217
|
}
|
|
1131
1218
|
|
|
1132
|
-
//
|
|
1219
|
+
// no extra message syntax-unexpected-assoc for guard failure
|
|
1133
1220
|
associationInSelectItem( art ) {
|
|
1134
1221
|
if (art.name)
|
|
1135
1222
|
return;
|
|
@@ -1348,15 +1435,16 @@ function addOneForDefinition( count, ext ) {
|
|
|
1348
1435
|
|
|
1349
1436
|
// Significant digits (before exponent) without leading and trailing zeros
|
|
1350
1437
|
function relevantDigits( val ) {
|
|
1438
|
+
// eslint-disable-next-line sonarjs/slow-regex
|
|
1439
|
+
val = val.replace( /e.+$/i, '' ); // this regex has no newlines -> is not slow
|
|
1440
|
+
|
|
1351
1441
|
const init = /^[-+0.]+/g; // global flag to have lastIndex
|
|
1352
1442
|
const zeros = /[0.]+/g;
|
|
1353
1443
|
if (init.test( val )) // sets init.lastIndex
|
|
1354
1444
|
zeros.lastIndex = init.lastIndex;
|
|
1355
1445
|
|
|
1356
1446
|
let r;
|
|
1357
|
-
while ((r = zeros.exec( val )) != null &&
|
|
1358
|
-
zeros.lastIndex < val.length &&
|
|
1359
|
-
val.charAt( zeros.lastIndex ).toLowerCase() !== 'e')
|
|
1447
|
+
while ((r = zeros.exec( val )) != null && zeros.lastIndex < val.length)
|
|
1360
1448
|
;
|
|
1361
1449
|
return val.slice( init.lastIndex, r?.index ).replace( /\./, '' );
|
|
1362
1450
|
}
|