@sap/cds-compiler 5.4.2 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/bin/cds_remove_invalid_whitespace.js +4 -4
  3. package/bin/cds_update_annotations.js +3 -3
  4. package/bin/cds_update_identifiers.js +3 -3
  5. package/lib/api/main.js +18 -30
  6. package/lib/api/validate.js +6 -1
  7. package/lib/base/lazyload.js +28 -0
  8. package/lib/base/location.js +1 -0
  9. package/lib/base/message-registry.js +53 -11
  10. package/lib/base/messages.js +17 -3
  11. package/lib/checks/{dbFeatureFlags.js → featureFlags.js} +1 -1
  12. package/lib/checks/parameters.js +61 -4
  13. package/lib/checks/validator.js +14 -6
  14. package/lib/compiler/index.js +7 -7
  15. package/lib/compiler/shared.js +29 -13
  16. package/lib/gen/BaseParser.js +345 -235
  17. package/lib/gen/CdlParser.js +4434 -4492
  18. package/lib/gen/Dictionary.json +2 -2
  19. package/lib/json/to-csn.js +3 -1
  20. package/lib/language/antlrParser.js +2 -111
  21. package/lib/main.js +16 -37
  22. package/lib/modelCompare/utils/filter.js +47 -21
  23. package/lib/parsers/AstBuildingParser.js +59 -49
  24. package/lib/parsers/CdlGrammar.g4 +91 -130
  25. package/lib/parsers/index.js +123 -0
  26. package/lib/render/toSql.js +8 -2
  27. package/lib/render/utils/delta.js +33 -1
  28. package/lib/transform/db/{transformExists.js → assocsToQueries/transformExists.js} +12 -407
  29. package/lib/transform/db/assocsToQueries/utils.js +440 -0
  30. package/lib/transform/db/expansion.js +2 -2
  31. package/lib/transform/draft/db.js +14 -3
  32. package/lib/transform/effective/annotations.js +3 -3
  33. package/lib/transform/effective/main.js +5 -7
  34. package/lib/transform/featureFlags.js +5 -0
  35. package/lib/transform/forRelationalDB.js +125 -192
  36. package/lib/transform/odata/createForeignKeys.js +1 -1
  37. package/lib/transform/odata/flattening.js +1 -1
  38. package/lib/transform/transformUtils.js +0 -51
  39. package/package.json +2 -2
  40. package/lib/transform/db/featureFlags.js +0 -5
@@ -1089,8 +1089,7 @@
1089
1089
  "Type": "Core.Tag",
1090
1090
  "AppliesTo": [
1091
1091
  "EntityContainer"
1092
- ],
1093
- "$experimental": true
1092
+ ]
1094
1093
  },
1095
1094
  "Common.mediaUploadLink": {
1096
1095
  "Type": "Edm.String",
@@ -4475,6 +4474,7 @@
4475
4474
  "$kind": "ComplexType",
4476
4475
  "BaseType": "UI.DataFieldAbstract",
4477
4476
  "Properties": {
4477
+ "ID": "Edm.String",
4478
4478
  "Actions": "Collection(UI.DataFieldForActionAbstract)",
4479
4479
  "Label": "Edm.String",
4480
4480
  "Criticality": "UI.CriticalityType",
@@ -1202,7 +1202,9 @@ function exprInternal( node, xprParens ) {
1202
1202
  }
1203
1203
  if (node.path) {
1204
1204
  const ref = node.path.map( pathItem );
1205
- if (node.path.$prefix) // auto-corrected ORDER BY refs without table alias
1205
+ // auto-corrected ORDER BY refs without table alias, or EXTEND … WITH COLUMN
1206
+ // refs to source element shadowed by alias name:
1207
+ if (node.path.$prefix)
1206
1208
  ref.unshift( node.path.$prefix );
1207
1209
  // we would need to consider node.global here if we introduce that
1208
1210
  return extra( { ref }, node );
@@ -17,11 +17,6 @@ const { XsnSource } = require('../compiler/xsn-model');
17
17
  const Parser = require('../gen/languageParser').default;
18
18
  const Lexer = require('../gen/languageLexer').default;
19
19
 
20
- const CdlLexer = require( '../parsers/Lexer' );
21
- const CdlParser = require( '../gen/CdlParser' );
22
- const { createMessageFunctions } = require( '../base/messages' );
23
- const { CompilerAssertion } = require( '../base/error' );
24
-
25
20
  // Error listener used for ANTLR4-generated parser
26
21
  class ErrorListener extends antlr4.error.ErrorListener {
27
22
  // method which is called by generated parser with --trace-parser[-amg]:
@@ -125,18 +120,7 @@ function tokenTypeOf( recognizer, literalName ) {
125
120
  // the AST locations and error messages. If provided, `options` are compile
126
121
  // options.
127
122
 
128
- const rules = {
129
- cdl: { func: 'start', returns: 'source', $frontend: 'cdl' },
130
- query: { func: 'queryEOF', returns: 'query' },
131
- expr: { func: 'conditionEOF', returns: 'cond' }, // yes, condition
132
- };
133
-
134
- function parse( source, filename = '<undefined>.cds',
135
- options = {}, messageFunctions = null,
136
- rule = 'cdl' ) {
137
- if (options.newParser)
138
- return parseWithNewParser( source, filename, options, messageFunctions, rule );
139
-
123
+ function parse( source, filename, options, messageFunctions, rulespec ) {
140
124
  const lexer = new Lexer( new antlr4.InputStream(source) );
141
125
  const tokenStream = new RewriteTypeTokenStream(lexer);
142
126
  /** @type {object} */
@@ -179,10 +163,9 @@ function parse( source, filename = '<undefined>.cds',
179
163
  parser.addParseListener(options.parseListener);
180
164
 
181
165
 
182
- const rulespec = rules[rule];
183
166
  let tree;
184
167
  try {
185
- tree = rule && parser[rulespec.func]();
168
+ tree = parser[rulespec.func]();
186
169
  }
187
170
  catch (e) {
188
171
  if (e instanceof RangeError && e.message.match(/Maximum.*exceeded$/i)) {
@@ -219,96 +202,4 @@ function parse( source, filename = '<undefined>.cds',
219
202
  return ast;
220
203
  }
221
204
 
222
- function parseWithNewParser( source, filename, options, messageFunctions, rule ) {
223
- if (CdlParser.tracingParser) // tracing → direct console output of message
224
- messageFunctions = createMessageFunctions( {}, 'parse', {} );
225
- const lexer = new CdlLexer( filename, source );
226
- const parser = new CdlParser( lexer, options, messageFunctions ).init();
227
- parser.filename = filename; // LSP compatibility
228
-
229
- const { parseListener, attachTokens } = options;
230
- if (parseListener || attachTokens) {
231
- const combined = [];
232
- const { tokens, comments, docComments } = parser;
233
- const length = tokens.length + comments.length + docComments.length;
234
- let tokenIdx = 0;
235
- let commentIdx = 0;
236
- let docCommentIdx = 0;
237
- for (let index = 0; index < length; ++index) {
238
- if (tokens[tokenIdx].location.tokenIndex === index) // EOF has largest tokenIndex
239
- combined.push( tokens[tokenIdx++] );
240
- else if (comments[commentIdx]?.location.tokenIndex === index)
241
- combined.push( comments[commentIdx++] );
242
- else
243
- combined.push( docComments[docCommentIdx++] );
244
- }
245
- if (!combined.at( -1 ))
246
- throw new CompilerAssertion( 'Invalid values for `tokenIndex`' );
247
- for (const tok of combined)
248
- tok.start = lexer.characterPos( tok.location.line, tok.location.col );
249
-
250
- parser._input = { tokens: combined, lexer }; // lexer for characterPos() in cdshi.js
251
- parser.getTokenStream = function getTokenStream() {
252
- return this._input;
253
- };
254
- }
255
- // LSP feature: provide parse listener with ANTLR-like context:
256
- if (parseListener) {
257
- // TODO LSP: we could also call different listener methods: then LSP could
258
- // have dedicated methods for ANTLR-based and new parser
259
- parser.rule_ = function rule_( ...args ) {
260
- CdlParser.prototype.rule_.apply( this, args );
261
- let state = this.s;
262
- while (typeof this.table[--state] !== 'string')
263
- ;
264
- const $ctx = { // TODO LSP: more to add?
265
- parser: this, // set in generated ANTLR parser for each rule context
266
- ruleName: this.table[state], // instead of ruleIndex
267
- start: this.la(), // set in Parser#enterRule
268
- stop: null,
269
- };
270
- parser.stack.at( -1 ).$ctx = $ctx;
271
- parseListener.enterEveryRule( $ctx );
272
- };
273
- parser.exit_ = function exit_( ...args ) {
274
- const { $ctx } = parser.stack.at( -1 );
275
- // TODO: what should we do in case of errors?
276
- $ctx.stop = this.lb();
277
- parseListener.exitEveryRule( $ctx );
278
- return CdlParser.prototype.exit_.apply( this, args );
279
- };
280
- parser.c = function c( ...args ) { // consume
281
- const symbol = this.la();
282
- const result = CdlParser.prototype.c.apply( this, args );
283
- if (result)
284
- parseListener.visitTerminal( { symbol } );
285
- return result;
286
- };
287
- parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery
288
- const symbol = this.la();
289
- CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx`
290
- parseListener.visitErrorNode( { symbol } );
291
- };
292
- }
293
- const result = {};
294
- const rulespec = rules[rule];
295
- if (rulespec) {
296
- try {
297
- parser[rulespec.func]( result );
298
- }
299
- catch (e) {
300
- if (!(e instanceof RangeError && /Maximum.*exceeded$/i.test( e.message )))
301
- throw e;
302
- messageFunctions.error('syntax-invalid-source', { file: filename },
303
- { '#': 'cdl-stackoverflow' } );
304
- result[rulespec.returns] = undefined;
305
- }
306
- }
307
- const ast = result[rulespec?.returns] || (rule === 'cdl' ? new XsnSource( 'cdl' ) : {} );
308
- ast.options = options;
309
- if (attachTokens === true || attachTokens === filename)
310
- ast.tokenStream = parser._input;
311
- return ast;
312
- }
313
-
314
205
  module.exports = parse;
package/lib/main.js CHANGED
@@ -1,19 +1,23 @@
1
- // Main entry point for the CDS Compiler
1
+ // Main entry point for the CDS Compiler (API)
2
2
  //
3
- // File for external usage = which is read in other modules with
4
- // require('cdsv');
3
+ // Other NPM modules must not require any other files than this one.
5
4
 
6
5
  // Proposed intra-module lib dependencies:
7
6
  // - lib/base/<file>.js: can be required by all others, requires no other
8
- // of this project
7
+ // of this project, except a lib/base/<other-file>.js
8
+ // - lib/util/<file>.js: TODO - clarify diff to lib/base/
9
9
  // - lib/<dir>/<file>.js: can be required by other files lib/<dir>/,
10
- // can require other files lib/<dir>/ and lib/base/<file>.js
10
+ // can require other files in lib/<dir>/ and lib/base/<file>.js,
11
+ // and lib/<other-dir>/ (the index.js in <other-dir>).
11
12
  // - lib/main.js (this file): can be required by none in lib/ (only in
12
13
  // bin/ and test/), can require any other
13
14
 
14
15
  'use strict';
15
16
 
17
+ const lazyload = require('./base/lazyload')( module );
18
+
16
19
  const { traceApi } = require('./api/trace');
20
+
17
21
  const snapi = lazyload('./api/main');
18
22
  const csnUtils = lazyload('./model/csnUtils');
19
23
  const model_api = lazyload('./model/api');
@@ -22,7 +26,7 @@ const sqlIdentifier = lazyload('./sql-identifier');
22
26
  const keywords = lazyload( './base/keywords' );
23
27
  const toCdl = lazyload('./render/toCdl');
24
28
 
25
- const parseLanguage = lazyload('./language/antlrParser');
29
+ const parsers = lazyload('./parsers');
26
30
  const compiler = lazyload('./compiler');
27
31
  const shared = lazyload('./compiler/shared');
28
32
  const define = lazyload('./compiler/define');
@@ -43,8 +47,8 @@ function parseCdl( cdlSource, filename, options = {} ) {
43
47
  const messageFunctions = messages.createMessageFunctions( options, 'parse', model );
44
48
  model.$messageFunctions = messageFunctions;
45
49
 
46
- const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
47
- messageFunctions );
50
+ const xsn = parsers.parseCdl( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
51
+ messageFunctions );
48
52
  sources[filename] = xsn;
49
53
  shared.fns( model );
50
54
  define( model );
@@ -55,16 +59,16 @@ function parseCdl( cdlSource, filename, options = {} ) {
55
59
 
56
60
  function parseCql( cdlSource, filename = '<query>.cds', options = {} ) {
57
61
  const messageFunctions = messages.createMessageFunctions( options, 'parse' );
58
- const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
59
- messageFunctions, 'query' );
62
+ const xsn = parsers.parseCdl( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
63
+ messageFunctions, 'query' );
60
64
  messageFunctions.throwWithError();
61
65
  return toCsn.compactQuery( xsn );
62
66
  }
63
67
 
64
68
  function parseExpr( cdlSource, filename = '<expr>.cds', options = {} ) {
65
69
  const messageFunctions = messages.createMessageFunctions( options, 'parse' );
66
- const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
67
- messageFunctions, 'expr' );
70
+ const xsn = parsers.parseCdl( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
71
+ messageFunctions, 'expr' );
68
72
  messageFunctions.throwWithError();
69
73
  return toCsn.compactExpr( xsn );
70
74
  }
@@ -191,28 +195,3 @@ module.exports = {
191
195
  isInReservedNamespace: (...args) => builtins.isInReservedNamespace(...args),
192
196
  },
193
197
  };
194
-
195
- /**
196
- * Load the module on-demand and not immediately.
197
- *
198
- * @param {string} moduleName Name of the module to load - like with require
199
- * @returns {object} A Proxy that handles the on-demand loading
200
- */
201
- function lazyload(moduleName) {
202
- let module;
203
- return new Proxy(((...args) => {
204
- if (!module)
205
- module = require(moduleName);
206
-
207
- if (module.apply && typeof module.apply === 'function')
208
- return module.apply(this, args);
209
- return module; // for destructured calls
210
- }), {
211
- get(target, name) {
212
- if (!module)
213
- module = require(moduleName);
214
-
215
- return module[name];
216
- },
217
- });
218
- }
@@ -61,6 +61,7 @@ module.exports = {
61
61
  function getFilterObject( options, dialect, extensionCallback, migrationCallback, removeConstraintsCallback, primaryKeyCallback ) {
62
62
  const context = { hasLossyChanges: false };
63
63
  const raiseErrorOrMarkAsLossy = getSafeguardManager(context, options);
64
+ const messageVariant = options.script ? 'script' : 'std';
64
65
  return {
65
66
  // will be called with a simple Array.filter, as we need to filter constraint `ADD` for SQLite
66
67
  extension: ({
@@ -69,7 +70,7 @@ function getFilterObject( options, dialect, extensionCallback, migrationCallback
69
70
  let returnValue = true;
70
71
  forEach(elements, (name, element) => {
71
72
  if (dialect !== 'sqlite' && isKey(element))
72
- message('type-unsupported-key-change', [ 'definitions', extend, 'elements', name ], { id: name, '#': 'std' } );
73
+ message('migration-unsupported-key-change', [ 'definitions', extend, 'elements', name ], { id: name, '#': 'std' } );
73
74
  else if (extensionCallback && !extensionCallback(extend, name, element, { error, warning }))
74
75
  returnValue = false;
75
76
  });
@@ -82,23 +83,31 @@ function getFilterObject( options, dialect, extensionCallback, migrationCallback
82
83
  // will be called with a Array.forEach
83
84
  migration: (migrations, { error, warning, message }) => {
84
85
  forEach(migrations.remove, (name, migration) => {
85
- raiseErrorOrMarkAsLossy(migration, () => error('def-unsupported-element-drop', [ 'definitions', migrations.migrate, 'elements', name ], {}, 'Dropping elements is not supported'));
86
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-element-drop', id => message(id, [ 'definitions', migrations.migrate, 'elements', name ], { '#': messageVariant }));
86
87
  });
87
88
 
88
89
  forEach(migrations.change, (name, migration) => {
89
90
  const loc = [ 'definitions', migrations.migrate, 'elements', name ];
90
- if (migration.new.type === migration.old.type && migration.new.length < migration.old.length)
91
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-length-change', loc, { id: name }, 'Changed element $(ID) is a length reduction and is not supported'));
92
- else if (migration.new.type === migration.old.type && migration.new.scale !== migration.old.scale)
93
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-scale-change', loc, { id: name }, 'Changed element $(ID) is a scale change and is not supported'));
94
- else if (migration.new.type === migration.old.type && migration.new.precision !== migration.old.scale)
95
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-precision-change', loc, { id: name }, 'Changed element $(ID) is a precision change and is not supported'));
96
- else if (migration.new.type !== migration.old.type && typeChangeIsNotCompatible(dialect, migration.old.type, migration.new.type))
97
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-change', loc, { id: name, name: migration.old.type, type: migration.new.type }, 'Changed element $(ID) is a lossy type change from $(NAME) to $(TYPE) and is not supported'));
98
- else if (dialect !== 'sqlite' && isKey(migration.new) && !isKey(migration.old)) // key added/changed - pg, hana and sqlite do not support it, h2 probably also - issues when data is in the table already
99
- raiseErrorOrMarkAsLossy(migration, () => message('type-unsupported-key-change', [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } ));
100
- else if (migrationCallback)
91
+ if (migration.new.type === migration.old.type && migration.new.length < migration.old.length) {
92
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-length-change', id => message(id, loc, { '#': messageVariant, id: name }));
93
+ }
94
+ else if (migration.new.type === migration.old.type && migration.new.scale !== migration.old.scale) {
95
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-scale-change', id => message(id, loc, { '#': messageVariant, id: name }));
96
+ }
97
+ else if (migration.new.type === migration.old.type && migration.new.precision !== migration.old.scale) {
98
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-precision-change', id => message(id, loc, { '#': messageVariant, id: name }));
99
+ }
100
+ else if (migration.new.type !== migration.old.type && typeChangeIsNotCompatible(dialect, migration.old.type, migration.new.type)) {
101
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-change', id => message(id, loc, {
102
+ '#': messageVariant, id: name, name: migration.old.type, type: migration.new.type,
103
+ }));
104
+ }
105
+ else if (dialect !== 'sqlite' && isKey(migration.new) && !isKey(migration.old)) { // key added/changed - pg, hana and sqlite do not support it, h2 probably also - issues when data is in the table already
106
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-key-change', id => message( id, [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } ));
107
+ }
108
+ else if (migrationCallback) {
101
109
  migrationCallback(migrations.migrate, name, migration, migrations.change, error);
110
+ }
102
111
 
103
112
  if (options.script && migration.lossy && migrationCallback)
104
113
  migrationCallback(migrations.migrate, name, migration, migrations.change, error);
@@ -115,9 +124,9 @@ function getFilterObject( options, dialect, extensionCallback, migrationCallback
115
124
  });
116
125
  }
117
126
  },
118
- deletion: ([ artifactName, artifact ], error) => {
127
+ deletion: ([ artifactName, artifact ], { message }) => {
119
128
  if (isPersistedAsTable(artifact))
120
- raiseErrorOrMarkAsLossy(artifact, () => error('def-unsupported-table-drop', [ 'definitions', artifactName ], 'Dropping tables is not supported'));
129
+ raiseErrorOrMarkAsLossy(artifactName, artifact, 'migration-unsupported-table-drop', id => message(id, [ 'definitions', artifactName ], { '#': messageVariant }));
121
130
  },
122
131
  changedPrimaryKeys: (changedPrimaryKeyArtifactName) => {
123
132
  if (primaryKeyCallback)
@@ -191,13 +200,30 @@ function filterCsn( csn ) {
191
200
  }
192
201
 
193
202
  function getSafeguardManager( context, options ) {
194
- return function raiseErrorOrMarkAsLossy(migration, raiseError) {
195
- if (!options.script) {
196
- raiseError();
197
- }
198
- else {
199
- migration.lossy = true;
203
+ return function raiseErrorOrMarkAsLossy(name, migration, id, raiseMessage) {
204
+ raiseMessage(id);
205
+
206
+ if (options.script) {
207
+ migration.details = getDetails(id, name);
208
+ migration.lossy = id !== 'migration-unsupported-key-change';
200
209
  context.hasLossyChanges = true;
201
210
  }
202
211
  };
203
212
  }
213
+
214
+ const details = {
215
+ 'migration-unsupported-element-drop': 'drop of element',
216
+ 'migration-unsupported-length-change': 'length reduction of element',
217
+ 'migration-unsupported-scale-change': 'scale reduction of element',
218
+ 'migration-unsupported-precision-change': 'precision reduction of element',
219
+ 'migration-unsupported-change': 'incompatible type change of element',
220
+ 'migration-unsupported-key-change': 'key property change of element',
221
+ 'migration-unsupported-table-drop': 'drop of entity',
222
+ };
223
+
224
+ function getDetails(id, name) {
225
+ if (details[id])
226
+ return `${details[id] } "${ name }" - check warnings for details`;
227
+
228
+ return null;
229
+ }
@@ -70,37 +70,31 @@ class AstBuildingParser extends BaseParser {
70
70
  return this.$messageFunctions.info( id, location?.location || location, args, text );
71
71
  }
72
72
 
73
- expectingArray( token ) {
74
- const expecting = this._expecting( token );
75
- let array = Object.keys( expecting );
76
- // compatibility: replace true+false by Boolean
77
- if (expecting.true && expecting.false)
73
+ expectingArray() {
74
+ let array = this.expectingArray_();
75
+ // compatibility: replace true+false by Boolean - TODO: delete
76
+ if (array.includes( 'true' ))
78
77
  array = [ 'Boolean', ...array.filter( n => n !== 'true' && n !== 'false' ) ];
79
78
  return array.map( antlrName )
80
79
  .sort( (a, b) => (tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1) );
81
80
  }
82
81
 
83
- reportUnexpectedToken_( token ) {
84
- const expecting = this.expectingArray( token );
82
+ reportUnexpectedToken_() {
83
+ const token = this.la();
84
+ const expecting = this.expectingArray();
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'
88
88
  err.expectedTokens = expecting;
89
89
  }
90
- reportReservedWord_( token ) {
90
+ reportReservedWord_() {
91
+ const token = this.la();
91
92
  const err = this.message( 'syntax-unexpected-reserved-word', token,
92
93
  { code: token.text, delimited: token.text } );
93
94
  // TODO: at least if one expected keyword is similar, mention expected set
94
95
  err.expectedTokens = this.expectingArray();
95
96
  }
96
97
 
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
98
  tableWithoutAs() {
105
99
  // TODO TOOL: if the tool properly creates `default: this.giR()`, this
106
100
  // condition method is most likely not necessary
@@ -180,7 +174,7 @@ class AstBuildingParser extends BaseParser {
180
174
  return this.lGenericIntroOrExpr( false );
181
175
  }
182
176
 
183
- lGenericSeparator() {
177
+ lGenericSeparator() { // TODO: { keyword, type } as arg ?
184
178
  const { keyword, type } = this.la();
185
179
  // TODO: use lower-case in specialFunctions
186
180
  const text = typeof keyword === 'string' ? keyword.toUpperCase() : type;
@@ -191,7 +185,7 @@ class AstBuildingParser extends BaseParser {
191
185
  translateParserToken_( tokenName ) {
192
186
  const realTokens = this.dynamic_.generic?.[parserTokens[tokenName]];
193
187
  // TODO: avoid parserTokens dict, use lower-case in specialFunctions
194
- return realTokens?.map( s => s.toLowerCase() ) ?? [];
188
+ return realTokens?.map( s => s.toLowerCase() ) ?? [ tokenName ];
195
189
  }
196
190
 
197
191
  inSelectItem( _test, arg ) { // only as action
@@ -211,14 +205,14 @@ class AstBuildingParser extends BaseParser {
211
205
  return true;
212
206
  // TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
213
207
  // (as <prepare>)
214
- const next = this.tokens[this.tokenIdx + 1].type;
208
+ const next = this.tokens[this.tokenIdx + 1]?.type;
215
209
  return next !== '*' && next !== '{';
216
210
  }
217
211
 
218
212
  // <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
219
213
  // completion for `… default 3 not ~;` → currently just `null` but hey
220
214
  isNegatedRelation( _test, prec ) {
221
- return this.tokens[this.tokenIdx + 1].keyword !== 'null' &&
215
+ return this.tokens[this.tokenIdx + 1]?.keyword !== 'null' &&
222
216
  this.precNone_( _test, prec );
223
217
  }
224
218
 
@@ -261,13 +255,13 @@ class AstBuildingParser extends BaseParser {
261
255
  *
262
256
  * Called as <prepare=…>:
263
257
  *
264
- * - <…, arg=default> in `returnsSpec`: after `returns`
265
- * disallow `default` in `typeExpression`
266
258
  * - <…,arg=elem> in `elementDef` (before calling `typeExpression`):
267
259
  * allow `default`/`= calcExpr` with final annotation assignments,
268
260
  * delay final doc comment
261
+ * - <…, arg=default> in `returnsSpec`: after `returns`
262
+ * disallow `default` in `typeExpression`
269
263
  * - <…, arg=calc> in `typeExpression` (with associations, etc)
270
- * now disallow `= calcExpr` (with annotation assignments) in `elementDef`,
264
+ * now disallow `= calcExpr` in `elementDef`,
271
265
  * do not delay final doc comments anymore
272
266
  * - <…, arg=anno> in `typeExpression` after enums:
273
267
  * now disallow annotation assignments after `= calcExpr`,
@@ -275,25 +269,48 @@ class AstBuildingParser extends BaseParser {
275
269
  *
276
270
  * Called as <cond=…>:
277
271
  *
278
- * - <…, arg=default> in `nullabilityAndDefault`:
279
- * is `default` allowed? If used, disallow calc (with anno assignments)
272
+ * - <…, arg=default> in `typeExpression` and `typeProperties`
273
+ * is `default` allowed? If used, disallow calc and further DEFAULT
274
+ * - <…, arg=notNull> in `typeExpression` and `typeProperties`
275
+ * is `null`/`not null` allowed? ensures that it is only used once
280
276
  * - <…, arg=calc> in `elementDef`:
281
- * is `= calcExpr` allowed? If so, check for final doc
277
+ * is `= calcExpr` allowed? not with struct, assoc or MANY…
282
278
  * - <…, arg=anno> in `elementDef`:
283
- * are annotation assignments after `= calcExpr` allowed?
279
+ * are annotation assignments after `= calcExpr` allowed? not with ENUM…
280
+ *
281
+ * The value of the dynamic var `elementCtx` looks like [REJECTED, DEFAULT,
282
+ * NOTNULL] where
283
+ *
284
+ * - REJECTED is the string containing a to-be-rejected test `arg`
285
+ * - DEFAULT: true if `default` had been provided
286
+ * - NOTNULL: true if `null` or `not null` had been provided
284
287
  */
285
288
  elementRestriction( test, arg ) {
286
- const { elementCtx } = this.dynamic_;
289
+ let { elementCtx } = this.dynamic_;
287
290
  if (test) {
288
- if (!elementCtx || elementCtx[0] === arg)
289
- return !elementCtx;
291
+ if (elementCtx?.[0] === arg)
292
+ return false;
293
+ if (!elementCtx) { // with type, param, or annotation defs
294
+ // eslint-disable-next-line no-multi-assign
295
+ elementCtx = this.dynamic_.elementCtx = [ null, false, false ];
296
+ }
290
297
  if (arg === 'default') {
298
+ if (elementCtx[1])
299
+ return false;
300
+ elementCtx[1] = true;
291
301
  elementCtx[0] = 'calc';
292
302
  this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr
293
303
  }
304
+ else if (arg === 'notNull') {
305
+ if (elementCtx[2]) {
306
+ if (this.la().keyword !== elementCtx[2] || test === 'M') // TODO v6: always error
307
+ return false; // error if different nullibility specification
308
+ }
309
+ elementCtx[2] = this.la().keyword;
310
+ }
294
311
  }
295
312
  else if (arg === 'elem' || arg === 'default') {
296
- this.dynamic_.elementCtx = [ arg ];
313
+ this.dynamic_.elementCtx = [ arg, false, false ];
297
314
  }
298
315
  else if (elementCtx) {
299
316
  elementCtx[0] = arg;
@@ -737,22 +754,10 @@ class AstBuildingParser extends BaseParser {
737
754
  * - misplaced doc comments would lead to a parse error (incompatible),
738
755
  * - would influence the prediction and error recovery,
739
756
  * - is only slightly "more declarative" in the grammar.
740
- *
741
- * With argument `delayed`, potentially delay the doc processing.
742
- * See also `elementRestriction`.
743
757
  */
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
-
758
+ docComment( art ) {
754
759
  const { line: prevLine, col: prevCol } = this.lb()?.location ?? { line: 0, col: 0 };
755
- const { line: currLine, col: currCol } = this.la().location;
760
+ const { line: currLine, col: currCol } = (this.la() ?? this.lb()).location;
756
761
  let token;
757
762
  for (;;) {
758
763
  token = this.docComments[this.docCommentIndex];
@@ -801,13 +806,16 @@ class AstBuildingParser extends BaseParser {
801
806
  setNullability( art, val, location = this.lb().location ) {
802
807
  const notNull = { val, location };
803
808
  if (art.notNull) {
804
- this.reportDuplicateClause( 'notNull', art.notNull, notNull,
809
+ // complain about the second
810
+ this.reportDuplicateClause( 'notNull', notNull, art.notNull,
805
811
  (val ? 'not null' : 'null') );
806
812
  }
807
- art.notNull = notNull;
813
+ else {
814
+ art.notNull = notNull;
815
+ }
808
816
  }
809
817
 
810
- setAssocAndComposition( art, assoc, card, target ) {
818
+ setAssocAndComposition( art, assoc, card, target = {} ) {
811
819
  const { location } = assoc;
812
820
  art.type = {
813
821
  path: [ { id: keywordTypeNames[assoc.keyword], location } ],
@@ -816,7 +824,7 @@ class AstBuildingParser extends BaseParser {
816
824
  };
817
825
  art.target = target;
818
826
  if (!card)
819
- return;
827
+ return target;
820
828
 
821
829
  const targetMax = (card.keyword === 'one')
822
830
  ? { val: 1, literal: 'number', location: card.location }
@@ -829,6 +837,7 @@ class AstBuildingParser extends BaseParser {
829
837
  else {
830
838
  art.cardinality = { targetMax, location: targetMax.location };
831
839
  }
840
+ return target;
832
841
  }
833
842
 
834
843
  reportExpandInline( column, isInline ) {
@@ -868,9 +877,10 @@ class AstBuildingParser extends BaseParser {
868
877
  col: chosen.location.col,
869
878
  };
870
879
  if (erroneous.val === chosen.val) {
880
+ // TODO v6: duplicate clause = error, independently whether it is the same
871
881
  this.warning( 'syntax-duplicate-equal-clause', erroneous.location, args );
872
882
  }
873
- else {
883
+ else if (prop !== 'notNull') { // already via guard in grammar
874
884
  if (literalValIfNotEq)
875
885
  args.code = chosen.val;
876
886
  this.message( 'syntax-duplicate-clause', erroneous.location, args );