@sap/cds-compiler 6.8.0 → 6.9.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 (42) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +4 -0
  3. package/bin/cdshi.js +1 -0
  4. package/lib/api/main.js +8 -1
  5. package/lib/api/options.js +3 -1
  6. package/lib/base/builtins.js +13 -9
  7. package/lib/base/csnRefs.js +8 -10
  8. package/lib/base/message-registry.js +61 -2
  9. package/lib/base/messages.js +2 -0
  10. package/lib/base/optionProcessor.js +2 -0
  11. package/lib/base/specialOptions.js +1 -1
  12. package/lib/compiler/assert-consistency.js +11 -9
  13. package/lib/compiler/base.js +5 -1
  14. package/lib/compiler/define.js +1 -1
  15. package/lib/compiler/dictionaries.js +2 -3
  16. package/lib/compiler/extend.js +119 -19
  17. package/lib/compiler/lsp-api.js +3 -3
  18. package/lib/compiler/populate.js +4 -5
  19. package/lib/compiler/resolve.js +50 -35
  20. package/lib/compiler/shared.js +33 -12
  21. package/lib/compiler/tweak-assocs.js +2 -2
  22. package/lib/compiler/utils.js +26 -23
  23. package/lib/compiler/xpr-rewrite.js +2 -2
  24. package/lib/edm/EdmPrimitiveTypeDefinitions.js +4 -1
  25. package/lib/edm/annotations/genericTranslation.js +49 -6
  26. package/lib/gen/BaseParser.js +59 -97
  27. package/lib/gen/CdlGrammar.checksum +1 -1
  28. package/lib/gen/CdlParser.js +2055 -1969
  29. package/lib/gen/Dictionary.json +67 -7
  30. package/lib/json/from-csn.js +7 -12
  31. package/lib/json/to-csn.js +59 -35
  32. package/lib/parsers/AstBuildingParser.js +43 -30
  33. package/lib/render/toCdl.js +46 -27
  34. package/lib/render/toSql.js +9 -0
  35. package/lib/render/utils/common.js +3 -2
  36. package/lib/tool-lib/enrichCsn.js +1 -0
  37. package/lib/transform/effective/flattening.js +6 -5
  38. package/lib/transform/effective/main.js +5 -0
  39. package/lib/transform/forOdata.js +20 -2
  40. package/lib/transform/forRelationalDB.js +8 -4
  41. package/lib/transform/tupleExpansion.js +40 -0
  42. package/package.json +3 -40
@@ -170,6 +170,8 @@
170
170
  "Type": "Core.Tag"
171
171
  },
172
172
  "Capabilities.BatchContinueOnErrorSupported": {
173
+ "$deprecated": true,
174
+ "$deprecationText": "Deprecated in favor of the [`ContinueOnErrorSupported`](#BatchSupportType) property from the [`BatchSupport`](#BatchSupport) term",
173
175
  "AppliesTo": [
174
176
  "EntityContainer"
175
177
  ],
@@ -1167,13 +1169,15 @@
1167
1169
  },
1168
1170
  "Communication.IsEmailAddress": {
1169
1171
  "AppliesTo": [
1170
- "Property"
1172
+ "Property",
1173
+ "Parameter"
1171
1174
  ],
1172
1175
  "Type": "Core.Tag"
1173
1176
  },
1174
1177
  "Communication.IsPhoneNumber": {
1175
1178
  "AppliesTo": [
1176
- "Property"
1179
+ "Property",
1180
+ "Parameter"
1177
1181
  ],
1178
1182
  "Type": "Core.Tag"
1179
1183
  },
@@ -1863,7 +1867,6 @@
1863
1867
  }
1864
1868
  }
1865
1869
  },
1866
- "$experimental": true,
1867
1870
  "AppliesTo": [
1868
1871
  "Property",
1869
1872
  "Parameter"
@@ -2106,7 +2109,8 @@
2106
2109
  "UI.Placeholder": {
2107
2110
  "AppliesTo": [
2108
2111
  "Property",
2109
- "Parameter"
2112
+ "Parameter",
2113
+ "Record"
2110
2114
  ],
2111
2115
  "Type": "Edm.String"
2112
2116
  },
@@ -2145,7 +2149,11 @@
2145
2149
  "AppliesTo": [
2146
2150
  "EntityType"
2147
2151
  ],
2148
- "Type": "Edm.ComplexType"
2152
+ "DerivedTypeConstraint": [
2153
+ "Edm.ComplexType",
2154
+ "Edm.EntityType"
2155
+ ],
2156
+ "Type": "Edm.Untyped"
2149
2157
  },
2150
2158
  "UI.SelectionFields": {
2151
2159
  "AppliesTo": [
@@ -2847,6 +2855,36 @@
2847
2855
  "TypecastSegmentSupported": "Edm.Boolean"
2848
2856
  }
2849
2857
  },
2858
+ "Capabilities.ExpandByKeyRestrictionsBase": {
2859
+ "$kind": "ComplexType",
2860
+ "BaseType": "Capabilities.ExpandRestrictionsBase",
2861
+ "Properties": {
2862
+ "Expandable": "Edm.Boolean",
2863
+ "MaxLevels": "Edm.Int32",
2864
+ "StreamsExpandable": "Edm.Boolean"
2865
+ }
2866
+ },
2867
+ "Capabilities.ExpandByKeyRestrictionsType": {
2868
+ "$kind": "ComplexType",
2869
+ "BaseType": "Capabilities.ExpandByKeyRestrictionsBase",
2870
+ "Properties": {
2871
+ "Expandable": "Edm.Boolean",
2872
+ "MaxLevels": "Edm.Int32",
2873
+ "NonExpandableProperties": "Collection(Edm.NavigationPropertyPath)",
2874
+ "NonExpandableStreamProperties": "Collection(Edm.PropertyPath)",
2875
+ "StreamsExpandable": "Edm.Boolean"
2876
+ }
2877
+ },
2878
+ "Capabilities.ExpandCollectionRestrictionsType": {
2879
+ "$kind": "ComplexType",
2880
+ "BaseType": "Capabilities.ExpandRestrictionsBase",
2881
+ "Properties": {
2882
+ "ExpandByKeyRestrictions": "Capabilities.ExpandByKeyRestrictionsBase",
2883
+ "Expandable": "Edm.Boolean",
2884
+ "MaxLevels": "Edm.Int32",
2885
+ "StreamsExpandable": "Edm.Boolean"
2886
+ }
2887
+ },
2850
2888
  "Capabilities.ExpandRestrictionsBase": {
2851
2889
  "$kind": "ComplexType",
2852
2890
  "Properties": {
@@ -2857,8 +2895,9 @@
2857
2895
  },
2858
2896
  "Capabilities.ExpandRestrictionsType": {
2859
2897
  "$kind": "ComplexType",
2860
- "BaseType": "Capabilities.ExpandRestrictionsBase",
2898
+ "BaseType": "Capabilities.ExpandCollectionRestrictionsType",
2861
2899
  "Properties": {
2900
+ "ExpandByKeyRestrictions": "Capabilities.ExpandByKeyRestrictionsBase",
2862
2901
  "Expandable": "Edm.Boolean",
2863
2902
  "MaxLevels": "Edm.Int32",
2864
2903
  "NonExpandableProperties": "Collection(Edm.NavigationPropertyPath)",
@@ -4463,7 +4502,28 @@
4463
4502
  "CriticalityRepresentation": "UI.CriticalityRepresentationType",
4464
4503
  "IconUrl": "Edm.String",
4465
4504
  "Label": "Edm.String",
4466
- "Value": "Edm.Untyped"
4505
+ "Value": {
4506
+ "DerivedTypeConstraint": [
4507
+ "Edm.PrimitiveType",
4508
+ "Collection(Edm.Binary)",
4509
+ "Collection(Edm.Boolean)",
4510
+ "Collection(Edm.Byte)",
4511
+ "Collection(Edm.Date)",
4512
+ "Collection(Edm.DateTimeOffset)",
4513
+ "Collection(Edm.Decimal)",
4514
+ "Collection(Edm.Double)",
4515
+ "Collection(Edm.Duration)",
4516
+ "Collection(Edm.Guid)",
4517
+ "Collection(Edm.Int16)",
4518
+ "Collection(Edm.Int32)",
4519
+ "Collection(Edm.Int64)",
4520
+ "Collection(Edm.SByte)",
4521
+ "Collection(Edm.Single)",
4522
+ "Collection(Edm.String)",
4523
+ "Collection(Edm.TimeOfDay)"
4524
+ ],
4525
+ "Type": "Edm.Untyped"
4526
+ }
4467
4527
  }
4468
4528
  },
4469
4529
  "UI.DataFieldAbstract": {
@@ -117,7 +117,7 @@
117
117
 
118
118
  const { dictAdd } = require('../compiler/dictionaries');
119
119
  const { quotedLiteralPatterns } = require('../compiler/builtins');
120
- const { isAnnotationExpression } = require('../base/builtins');
120
+ const { exprProperties } = require('../base/builtins');
121
121
  const { CompilerAssertion } = require('../base/error');
122
122
  const { Location } = require('../base/location');
123
123
  const { XsnSource } = require('../compiler/xsn-model');
@@ -141,11 +141,6 @@ const typeProperties = [
141
141
  'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull', 'default',
142
142
  'keys', 'on', // only with 'target'
143
143
  ];
144
- const exprProperties = [
145
- // do not include CSN v0.1.0 properties here:
146
- 'ref', 'xpr', 'list', 'val', '#', 'func', 'SELECT', 'SET', // Core Compiler checks SELECT/SET
147
- 'param', 'literal', 'args', 'cast', // only with 'ref'/'ref'/'val'/'func'
148
- ];
149
144
 
150
145
  // Groups of properties which cannot be used together:
151
146
  const xorGroups = {
@@ -619,16 +614,16 @@ const schema = compileSchema( {
619
614
  optional: exprProperties,
620
615
  },
621
616
  where: {
622
- class: 'condition',
617
+ class: 'condition', inKind: [ 'extend' ],
623
618
  },
624
619
  groupBy: {
625
- arrayOf: expr, optional: exprProperties,
620
+ arrayOf: expr, optional: exprProperties, inKind: [ 'extend' ],
626
621
  },
627
622
  having: {
628
- class: 'condition',
623
+ class: 'condition', inKind: [ 'extend' ],
629
624
  },
630
625
  orderBy: {
631
- arrayOf: expr, optional: [ 'sort', 'nulls', ...exprProperties ],
626
+ arrayOf: expr, optional: [ 'sort', 'nulls', ...exprProperties ], inKind: [ 'extend' ],
632
627
  },
633
628
  sort: {
634
629
  type: stringVal,
@@ -637,7 +632,7 @@ const schema = compileSchema( {
637
632
  type: stringVal, // TODO: test for valid ones?
638
633
  },
639
634
  limit: {
640
- type: object, requires: 'rows', optional: [ 'rows', 'offset' ],
635
+ type: object, requires: 'rows', optional: [ 'rows', 'offset' ], inKind: [ 'extend' ],
641
636
  },
642
637
  rows: {
643
638
  class: 'expression',
@@ -1467,7 +1462,7 @@ function annoValue( val, spec ) {
1467
1462
  ++virtualLine;
1468
1463
  return r;
1469
1464
  }
1470
- else if (isAnnotationExpression( val )) {
1465
+ else if (exprProperties.some( prop => val[prop] !== undefined )) {
1471
1466
  const s = schema['@'].schema['-expr'];
1472
1467
  const r = { location: location() };
1473
1468
  Object.assign( r, object( val, s ) );
@@ -234,11 +234,11 @@ function compactModel( model, options = model.options || {} ) {
234
234
  }
235
235
  // 'namespace' for complete model is 'namespace' of first source
236
236
  // (not a really useful property at all, avoids XSN inspection by Umbrella)
237
- for (const first in srcDict) {
237
+ const first = Object.keys( srcDict )[0];
238
+ if (first) {
238
239
  const { namespace } = srcDict[first];
239
240
  if (namespace?.name?.path)
240
241
  csn.namespace = pathName( namespace.name.path );
241
- break;
242
242
  }
243
243
  set( 'definitions', csn, model );
244
244
  if (Object.keys(model.vocabularies || {}).length > 0)
@@ -249,15 +249,16 @@ function compactModel( model, options = model.options || {} ) {
249
249
  set( 'i18n', csn, model );
250
250
  set( 'sources', csn, model );
251
251
  // Set $location, use $extra properties of first source as resulting $extra properties
252
- for (const first in srcDict) {
253
- const loc = srcDict[first].location;
252
+ const firstSourceName = Object.keys( srcDict )[0];
253
+ if (firstSourceName) {
254
+ const firstSource = srcDict[firstSourceName];
255
+ const loc = firstSource.location;
254
256
  if (loc && loc.file) {
255
257
  Object.defineProperty( csn, '$location', {
256
258
  value: { file: loc.file }, configurable: true, writable: true, enumerable: withLocations,
257
259
  } );
258
260
  }
259
- set( '$extra', csn, srcDict[first] );
260
- break;
261
+ set( '$extra', csn, firstSource );
261
262
  }
262
263
 
263
264
  const srcNames = Object.keys( srcDict );
@@ -273,7 +274,8 @@ function compactModel( model, options = model.options || {} ) {
273
274
  const compilerCsnFlavor = flavorMap[options.csnFlavor] ||
274
275
  (options.parseCdl ? 'parsed' : 'inferred');
275
276
  if (!options.testMode) {
276
- csn.meta = Object.assign( {}, model.meta, meta, { creator, compilerCsnFlavor } );
277
+ csn.meta = Object.assign( {}, model.meta, meta,
278
+ { creator, compilerVersion, compilerCsnFlavor } );
277
279
  csn.$version = csnVersion;
278
280
  }
279
281
  else if (meta !== undefined) {
@@ -322,16 +324,11 @@ function usings( srcDict ) {
322
324
  function extensions( node, csn, model ) {
323
325
  if (model.kind && model.kind !== 'source')
324
326
  return undefined;
325
- const exts = node.map( definition );
326
- if (gensrcFlavor) {
327
- for (const name of Object.getOwnPropertyNames( model.definitions || {} ).sort()) {
328
- const art = model.definitions[name];
329
- // From definitions (without redefinitions) with potential inferred elements:
330
- const result = { annotate: Object.create(null) };
331
- attachAnnotations( result, 'annotate', { [name]: art }, art.$inferred );
332
- if (result.annotate[name])
333
- exts.push( { annotate: name, ...result.annotate[name] } );
334
- }
327
+ let exts = node.map( definition );
328
+ if (gensrcFlavor && model.definitions) {
329
+ const gensrc = { annotate: Object.create( null ), extend: Object.create( null ) };
330
+ attachAnnotations( gensrc, null, model.definitions, null );
331
+ exts = exts.concat( Object.values( gensrc.annotate ), Object.values( gensrc.extend ) );
335
332
  }
336
333
  return exts.sort( // TODO: really sort with parse.cdl?
337
334
  (a, b) => (a.annotate || a.extend).localeCompare( b.annotate || b.extend )
@@ -378,36 +375,64 @@ function sources( srcDict, csn, model ) {
378
375
  }
379
376
  }
380
377
 
381
- function attachAnnotations( annotate, prop, dict, inferred, insideReturns = false ) {
382
- const annoDict = Object.create( dictionaryPrototype );
378
+ // create annotate and extend statements for CSN flavor gensrc
379
+ // TODO: rename this function - also refactor
380
+ function attachAnnotations( annotate, prop, dict, inferred, withExtend, insideReturns = false ) {
381
+ const annoDict = prop && Object.create( dictionaryPrototype );
383
382
  const names = Object.keys( dict );
384
383
  if (strictMode)
385
384
  names.sort();
386
385
  for (const name of names) {
387
386
  const entry = dict[name];
388
387
  const inf = inferred || entry.$inferred; // is probably always inferred if parent was
389
- const sub = (inf) ? annotationsAndDocComment( entry ) : {};
390
- if (entry.$expand === 'annotate') {
388
+ const withExt = withExtend || entry.$expand === 'extend';
389
+ const extProp = (entry.$expand === 'extend') ? 'extend' : 'annotate';
390
+ if (withExt && !entry.$inferred && entry.kind === 'enum') {
391
+ // currently just added enum - test for safety
392
+ if (dict[$inferred])
393
+ annoDict[name] = definition( entry );
394
+ continue;
395
+ }
396
+ let sub = {};
397
+ if (inf)
398
+ annotationsAndDocComment( entry, sub );
399
+
400
+ if (entry.$expand === 'annotate' || entry.$expand === 'extend') {
391
401
  if (entry.actions)
392
402
  attachAnnotations( sub, 'actions', entry.actions, inf );
393
403
  else if (entry.params)
394
404
  attachAnnotations( sub, 'params', entry.params, inf );
405
+
406
+ if (entry.$expand === 'extend' && (sub.actions || sub.params)) {
407
+ // there is only `annotate` with actions/params
408
+ annotate.annotate[name] = { annotate: name, ...sub };
409
+ sub = {};
410
+ }
411
+
395
412
  const obj = entry.returns || entry;
396
- const many = obj.items || obj;
413
+ const many = obj.items || obj; // TODO: many many?
397
414
  const elems = (many.targetAspect || many).elements;
398
- if (elems)
399
- attachAnnotations( sub, 'elements', elems, inf, entry.returns );
400
- else if (many.enum) // make 'enum' annotations appear in 'elements' annotate
401
- attachAnnotations( sub, 'elements', many.enum, inf, entry.returns );
402
- else if (entry.foreignKeys) // make 'foreignKeys' annotations appear in 'elements' annotate
403
- attachAnnotations( sub, 'elements', entry.foreignKeys, inf );
415
+ if (elems) {
416
+ attachAnnotations( sub, 'elements', elems, inf, withExt, entry.returns );
417
+ }
418
+ else if (many.enum) { // make 'enum' annotations appear in 'elements' annotate
419
+ attachAnnotations( sub, (withExt ? 'enum' : 'elements'),
420
+ many.enum, inf, withExt, entry.returns );
421
+ }
422
+ else if (entry.foreignKeys) { // make 'foreignKeys' annotations appear in 'elements' annotate
423
+ attachAnnotations( sub, 'elements', entry.foreignKeys, inf, withExt );
424
+ }
425
+ }
426
+ if (Object.keys( sub ).length > 0) {
427
+ if (prop)
428
+ annoDict[name] = (withExt) ? { kind: 'extend', ...sub } : sub;
429
+ else
430
+ annotate[extProp][name] = { [extProp]: name, ...sub };
404
431
  }
405
- if (Object.keys( sub ).length)
406
- annoDict[name] = sub;
407
432
  }
408
- if (Object.keys( annoDict ).length) {
433
+ if (prop && Object.keys( annoDict ).length) {
409
434
  if (insideReturns)
410
- annotate.returns = { elements: annoDict };
435
+ annotate.returns = { [prop]: annoDict };
411
436
  else
412
437
  annotate[prop] = annoDict;
413
438
  }
@@ -524,7 +549,7 @@ function enumerableQueryElements( select ) {
524
549
  return (universalCsn && select !== select._main._leadingQuery);
525
550
  }
526
551
 
527
- // Should we render the elements? (and items?)
552
+ // Should we render the elements? (and enum, items?)
528
553
  function keepElements( node, line ) {
529
554
  if (universalCsn)
530
555
  // $expand = null/undefined: not elements not via expansion
@@ -562,8 +587,7 @@ function keepElements( node, line ) {
562
587
  *
563
588
  * @param {object} node
564
589
  */
565
- function annotationsAndDocComment( node ) {
566
- const csn = {};
590
+ function annotationsAndDocComment( node, csn = {} ) {
567
591
  const transformer = transformers['@'];
568
592
  const keys = Object.keys( node ).filter( a => a.charAt(0) === '@' ).sort();
569
593
  for (const prop of keys) {
@@ -8,6 +8,7 @@ const { functionsWithoutParentheses } = require('./identifiers');
8
8
 
9
9
  const { pathName } = require('../compiler/utils');
10
10
  const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
11
+ const { primaryExprProperties } = require('../base/builtins');
11
12
 
12
13
  const { parseMultiLineStringLiteral } = require('../language/multiLineStringParser');
13
14
  const { normalizeNewLine, normalizeNumberString } = require('../language/textUtils');
@@ -100,11 +101,6 @@ const extensionsCode = {
100
101
  const PRECEDENCE_OF_EQUAL = 10;
101
102
 
102
103
  class AstBuildingParser extends BaseParser {
103
- leanConditions = {
104
- afterBrace: true,
105
- fail: true,
106
- };
107
-
108
104
  constructor( lexer, keywords, table, options, messageFunctions ) {
109
105
  super( lexer, keywords, table ); // lexer has file
110
106
  this.options = options;
@@ -201,7 +197,7 @@ class AstBuildingParser extends BaseParser {
201
197
  queryOnLeftSloppyAlias( _arg, mode ) {
202
198
  if (mode === 'M' || this.isNoKeywordInRuleFollow( _arg, mode ))
203
199
  return true;
204
- // TODO TOOL: have a base parser method for the test
200
+ // TODO TOOL: have a base parser method for the test - or extra mode (recovery)
205
201
  if (this.conditionTokenIdx === this.tokenIdx && // tested on same
206
202
  this.conditionStackLength == null) // after error recovery
207
203
  return false;
@@ -238,7 +234,7 @@ class AstBuildingParser extends BaseParser {
238
234
  }
239
235
 
240
236
  prepareSpecialFunction() {
241
- const func = this.tokens[this.tokenIdx - 2].keyword?.toUpperCase();
237
+ const func = this.lb(2).keyword?.toUpperCase();
242
238
  // TODO: use lower-case in specialFunctions
243
239
  const spec = specialFunctions[func];
244
240
  this.dynamic_.call = { func, argPos: 0 };
@@ -261,7 +257,7 @@ class AstBuildingParser extends BaseParser {
261
257
  if (generic !== 'expr')
262
258
  return (generic === 'intro') ? 'GenericIntro' : type;
263
259
  // if both intro and expr: specialFunctions[fn][argPos][token] = 'expr'
264
- const next = this.tokens[this.tokenIdx + 1];
260
+ const next = this.lb(-1);
265
261
  if (next && next.type !== ',' && next.type !== ')' &&
266
262
  this.dynamic_.generic[next.keyword?.toUpperCase?.()] !== 'separator')
267
263
  return 'GenericIntro';
@@ -309,8 +305,7 @@ class AstBuildingParser extends BaseParser {
309
305
  this.dynamic_.inSelectItem ??= [];
310
306
  this.dynamic_.inSelectItem = [ ...this.dynamic_.inSelectItem ];
311
307
  }
312
- this.dynamic_.inSelectItem.push(arg ||
313
- (this.tokens[this.tokenIdx - 2].type === '.' ? 'inline' : 'expand'));
308
+ this.dynamic_.inSelectItem.push(arg || (this.lb(2).type === '.' ? 'inline' : 'expand'));
314
309
  }
315
310
 
316
311
  /**
@@ -321,7 +316,7 @@ class AstBuildingParser extends BaseParser {
321
316
  modifierRestriction() {
322
317
  const top = this.dynamic_.inSelectItem?.at(-1);
323
318
  const insideExpand = this.dynamic_.inSelectItem?.includes('expand');
324
- const next = this.tokens[this.tokenIdx]?.keyword;
319
+ const next = this.la()?.keyword;
325
320
  return insideExpand || (next !== 'key' && top === 'inline');
326
321
  }
327
322
  modifierRestrictionError( args, offending ) {
@@ -340,7 +335,7 @@ class AstBuildingParser extends BaseParser {
340
335
  return false;
341
336
  // TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
342
337
  // (as <prepare>)
343
- const next = this.tokens[this.tokenIdx + 1]?.type;
338
+ const next = this.lb(-1)?.type;
344
339
  return next === '*' || next === '{';
345
340
  }
346
341
 
@@ -357,7 +352,7 @@ class AstBuildingParser extends BaseParser {
357
352
  // <prec=10, postfix=once> + test that the next token is not `null`; TODO: code
358
353
  // completion for `… default 3 not ~;` → currently just `null` but hey
359
354
  isNegatedRelation( prec ) {
360
- return this.tokens[this.tokenIdx + 1]?.keyword === 'null' ||
355
+ return this.lb(-1)?.keyword === 'null' ||
361
356
  this.precNone_( prec );
362
357
  }
363
358
 
@@ -372,7 +367,7 @@ class AstBuildingParser extends BaseParser {
372
367
  }
373
368
 
374
369
  isNamedArg() {
375
- const type = this.tokens[this.tokenIdx + 1]?.type;
370
+ const type = this.lb(-1)?.type;
376
371
  return type !== ':' && type !== '=>';
377
372
  }
378
373
 
@@ -522,13 +517,13 @@ class AstBuildingParser extends BaseParser {
522
517
  }
523
518
 
524
519
  noRepeatedCardinality( _arg, mode ) {
525
- if (this.tokens[this.tokenIdx - 2]?.type !== ']')
520
+ if (this.lb(2)?.type !== ']')
526
521
  return false;
527
522
  if (mode === 'M')
528
523
  return true;
529
524
  // currently just warning if same cardinality provided twice
530
525
  const same = { one: '1', many: '*' }[this.la().keyword];
531
- return this.tokens[this.tokenIdx - 3]?.text !== same;
526
+ return this.lb(3)?.text !== same;
532
527
  }
533
528
  noRepeatedCardinalityError( args ) {
534
529
  let openIdx = this.tokenIdx - 2;
@@ -606,7 +601,7 @@ class AstBuildingParser extends BaseParser {
606
601
  const { arrayAnno } = this.dynamic_;
607
602
  if (!arrayAnno[0])
608
603
  return arrayAnno[0] == null ? 'duplicate' : arg;
609
- arrayAnno[0] = this.tokens[this.tokenIdx + 1]?.keyword;
604
+ arrayAnno[0] = this.lb(-1)?.keyword;
610
605
  }
611
606
  else if (arg === 'bracket') { // syntax-invalid-ellipsis
612
607
  // closing bracket not allowed if last `...` in array is with `up to
@@ -628,7 +623,7 @@ class AstBuildingParser extends BaseParser {
628
623
  }
629
624
 
630
625
  beforeColon() {
631
- return this.tokens[this.tokenIdx + 1]?.text !== ':';
626
+ return this.lb(-1)?.text !== ':';
632
627
  }
633
628
 
634
629
  fail( _arg, mode ) {
@@ -652,13 +647,13 @@ class AstBuildingParser extends BaseParser {
652
647
  return;
653
648
  // assumes no value < -1:
654
649
  const location = (tokenAhead > 0)
655
- ? this.combineLocation( this.la(), this.tokens[this.tokenIdx + tokenAhead] )
656
- : this.tokens[this.tokenIdx + tokenAhead].location;
650
+ ? this.combineLocation( this.la(), this.lb(-tokenAhead) )
651
+ : this.lb(-tokenAhead).location;
657
652
  this.error( msgId, location, textArgs );
658
653
  }
659
654
 
660
655
  warnIfColonFollows( name ) {
661
- if (this.l() === ':') {
656
+ if (this.la().type === ':') {
662
657
  this.warning( 'syntax-missing-parens', name,
663
658
  { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
664
659
  // eslint-disable-next-line @stylistic/max-len
@@ -680,7 +675,7 @@ class AstBuildingParser extends BaseParser {
680
675
 
681
676
  reportDubiousAnnoSpacing() {
682
677
  const at = this.lb();
683
- const before = this.tokens[this.tokenIdx - 2];
678
+ const before = this.lb(2);
684
679
  if (before?.type === 'Id' && before.location.endLine === at.location.line &&
685
680
  before.location.endCol === at.location.col) {
686
681
  this.warning( 'syntax-expecting-anno-space', at.location, { code: '@' },
@@ -888,7 +883,7 @@ class AstBuildingParser extends BaseParser {
888
883
  classifyImplicitName( category, ref ) {
889
884
  if (!ref || ref.path) { // TODO: func
890
885
  const tokenIndex = ref?.path.at(-1)?.location.tokenIndex;
891
- const token = this.prevTokenWithIndex( tokenIndex ) ?? this.tokens[this.tokenIdx - 1];
886
+ const token = this.prevTokenWithIndex( tokenIndex ) ?? this.lb();
892
887
  const { parsedAs } = token;
893
888
  if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword')
894
889
  token.parsedAs = category;
@@ -1097,7 +1092,7 @@ class AstBuildingParser extends BaseParser {
1097
1092
 
1098
1093
  // TODO: can we remove `;`/EOF from the expected-set for `annotate Foo with ⎀`?
1099
1094
  checkWith( keyword ) {
1100
- if (this.lb() !== keyword || ![ ';', '}', 'EOF' ].includes( this.l() ))
1095
+ if (this.lb() !== keyword || ![ ';', '}', 'EOF' ].includes( this.la().type ))
1101
1096
  return;
1102
1097
  const tok = this.la();
1103
1098
  const docTokenIndex = this.docCommentIndex &&
@@ -1152,15 +1147,15 @@ class AstBuildingParser extends BaseParser {
1152
1147
  }
1153
1148
 
1154
1149
  // see also <guard=nestedExpand>
1155
- reportExpandInline( column, isInline ) {
1156
- // called before matching `{`
1150
+ reportExpandInline( column, sqlStyle, isInline ) {
1151
+ // called before matching `{` (or `*` with inline)
1157
1152
  if (column.value && !column.value.path) {
1158
1153
  // improve error location when using "inline" `.{…}` after ref (arguments and
1159
1154
  // filters not covered, not worth the effort); after an expression where
1160
1155
  // the last token is an identifier, not the `.` is wrong, but the `{`:
1161
- const token = (isInline && this.tokens[this.tokenIdx - 2].type !== 'Id')
1162
- ? this.lb()
1163
- : this.la();
1156
+ const token = (isInline && this.lb(2)?.type !== 'Id')
1157
+ ? this.lb() // e.g. with `func() .{` → the `.` is wrong
1158
+ : this.la(); // after `current_date` → the `{` is wrong
1164
1159
  this.error( 'syntax-unexpected-nested-proj', token,
1165
1160
  { code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }' },
1166
1161
  'Unexpected $(CODE); nested projections can only be used after a reference' );
@@ -1169,6 +1164,15 @@ class AstBuildingParser extends BaseParser {
1169
1164
  // - no errors for refs inside expand/inline, but for refs in sibling expr
1170
1165
  // - think about: reference to these (sub) elements from other view
1171
1166
  }
1167
+ else if (sqlStyle && this.la().text === '{') {
1168
+ this.message( 'syntax-invalid-nested-proj', this.la(),
1169
+ {
1170
+ code: isInline ? '.{ ‹inline› }' : '{ ‹expand› }',
1171
+ keyword: 'select from … { … }',
1172
+ },
1173
+ // eslint-disable-next-line @stylistic/max-len
1174
+ 'Unexpected $(CODE); nested projections can only be used within a CDL-style $(KEYWORD)' );
1175
+ }
1172
1176
  }
1173
1177
 
1174
1178
  reportDuplicateClause( prop, erroneous, chosen, code ) {
@@ -1198,6 +1202,15 @@ class AstBuildingParser extends BaseParser {
1198
1202
  }
1199
1203
  }
1200
1204
 
1205
+ checkStructProps( dict ) {
1206
+ const special = primaryExprProperties.find( prop => dict[prop] !== undefined );
1207
+ if (special || dict['=']) {
1208
+ const prop = special || '=';
1209
+ this.message( 'syntax-invalid-anno-struct', dict[prop].name.location,
1210
+ { '#': (special ? 'std' : 'ref'), prop } );
1211
+ }
1212
+ }
1213
+
1201
1214
  // TODO: remove the check from the parser; move it to shared.js
1202
1215
  checkTypeArgs( art ) {
1203
1216
  const args = art.$typeArgs;
@@ -1212,7 +1225,7 @@ class AstBuildingParser extends BaseParser {
1212
1225
  locationOfPrevTokens( offset ) {
1213
1226
  // TODO: use combined location of lb() and la() and move actions accordingly
1214
1227
  // (for error recovery)
1215
- const { file, line, col } = this.tokens[this.tokenIdx - offset].location;
1228
+ const { file, line, col } = this.lb( offset ).location;
1216
1229
  const { endLine, endCol } = this.lb().location;
1217
1230
  return {
1218
1231
  file,