@sap/cds-compiler 2.13.8 → 2.15.6

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 (78) hide show
  1. package/CHANGELOG.md +128 -4
  2. package/bin/cdsc.js +112 -37
  3. package/lib/api/main.js +63 -22
  4. package/lib/api/options.js +2 -3
  5. package/lib/api/validate.js +6 -6
  6. package/lib/base/message-registry.js +100 -17
  7. package/lib/base/messages.js +85 -64
  8. package/lib/base/optionProcessorHelper.js +19 -0
  9. package/lib/checks/annotationsOData.js +11 -32
  10. package/lib/checks/arrayOfs.js +1 -34
  11. package/lib/checks/validator.js +2 -4
  12. package/lib/compiler/assert-consistency.js +1 -0
  13. package/lib/compiler/base.js +1 -0
  14. package/lib/compiler/builtins.js +11 -0
  15. package/lib/compiler/checks.js +22 -70
  16. package/lib/compiler/define.js +59 -11
  17. package/lib/compiler/extend.js +20 -3
  18. package/lib/compiler/finalize-parse-cdl.js +26 -20
  19. package/lib/compiler/index.js +75 -26
  20. package/lib/compiler/populate.js +36 -17
  21. package/lib/compiler/propagator.js +4 -1
  22. package/lib/compiler/resolve.js +104 -16
  23. package/lib/compiler/shared.js +61 -27
  24. package/lib/compiler/tweak-assocs.js +7 -1
  25. package/lib/edm/annotations/genericTranslation.js +93 -21
  26. package/lib/edm/csn2edm.js +216 -98
  27. package/lib/edm/edm.js +305 -226
  28. package/lib/edm/edmPreprocessor.js +499 -423
  29. package/lib/edm/edmUtils.js +22 -22
  30. package/lib/gen/Dictionary.json +98 -22
  31. package/lib/gen/language.checksum +1 -1
  32. package/lib/gen/language.interp +3 -1
  33. package/lib/gen/languageParser.js +4636 -4368
  34. package/lib/json/csnVersion.js +10 -11
  35. package/lib/json/from-csn.js +3 -2
  36. package/lib/json/to-csn.js +0 -2
  37. package/lib/language/docCommentParser.js +2 -2
  38. package/lib/language/genericAntlrParser.js +47 -2
  39. package/lib/language/language.g4 +59 -27
  40. package/lib/main.d.ts +19 -1
  41. package/lib/main.js +6 -0
  42. package/lib/model/csnRefs.js +33 -6
  43. package/lib/model/csnUtils.js +193 -75
  44. package/lib/model/enrichCsn.js +1 -0
  45. package/lib/model/revealInternalProperties.js +2 -2
  46. package/lib/modelCompare/compare.js +6 -6
  47. package/lib/optionProcessor.js +62 -26
  48. package/lib/render/toCdl.js +844 -679
  49. package/lib/render/toHdbcds.js +189 -243
  50. package/lib/render/toSql.js +180 -198
  51. package/lib/render/utils/common.js +131 -15
  52. package/lib/transform/db/.eslintrc.json +1 -1
  53. package/lib/transform/db/associations.js +2 -2
  54. package/lib/transform/db/constraints.js +3 -1
  55. package/lib/transform/db/expansion.js +15 -10
  56. package/lib/transform/db/flattening.js +94 -64
  57. package/lib/transform/db/transformExists.js +7 -7
  58. package/lib/transform/db/views.js +6 -3
  59. package/lib/transform/forHanaNew.js +43 -26
  60. package/lib/transform/forOdataNew.js +43 -42
  61. package/lib/transform/localized.js +12 -7
  62. package/lib/transform/odata/toFinalBaseType.js +8 -6
  63. package/lib/transform/odata/typesExposure.js +145 -197
  64. package/lib/transform/transformUtilsNew.js +9 -12
  65. package/lib/transform/translateAssocsToJoins.js +5 -1
  66. package/lib/transform/universalCsn/coreComputed.js +5 -3
  67. package/lib/transform/universalCsn/universalCsnEnricher.js +27 -5
  68. package/lib/utils/moduleResolve.js +13 -6
  69. package/package.json +1 -1
  70. package/share/messages/message-explanations.json +2 -1
  71. package/share/messages/syntax-expected-integer.md +37 -0
  72. package/lib/transform/odata/attachPath.js +0 -96
  73. package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
  74. package/lib/transform/odata/generateForeignKeyElements.js +0 -261
  75. package/lib/transform/odata/referenceFlattener.js +0 -296
  76. package/lib/transform/odata/sortByAssociationDependency.js +0 -105
  77. package/lib/transform/odata/structuralPath.js +0 -72
  78. package/lib/transform/odata/structureFlattener.js +0 -171
@@ -128,30 +128,35 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
128
128
  // if (Object.getPrototypeOf( fileCache ))
129
129
  // fileCache = Object.assign( Object.create(null), fileCache );
130
130
  dir = path.resolve(dir);
131
- const a = processFilenames( filenames, dir );
132
-
133
- const model = { sources: a.sources, options };
131
+ const model = { sources: null, options };
134
132
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
135
- let all = promiseAllDoNotRejectImmediately( a.files.map(readAndParse) );
136
-
137
- all = all
133
+ let input = null;
134
+
135
+ let all = processFilenames( filenames, dir )
136
+ .then((processedInput) => {
137
+ input = processedInput;
138
+ model.sources = input.sources;
139
+ })
140
+ .then(() => promiseAllDoNotRejectImmediately( input.files.map(readAndParse) ))
138
141
  .then( testInvocation, (reason) => {
139
142
  // do not reject with PromiseAllError, use InvocationError:
140
143
  const errs = reason.valuesOrErrors.filter( e => e instanceof Error );
141
144
  // internal error if no file IO error (has property `path`)
142
145
  return Promise.reject( errs.find( e => !e.path ) ||
143
- new InvocationError( [ ...a.repeated, ...errs ]) );
146
+ new InvocationError( [ ...input.repeated, ...errs ]) );
144
147
  });
148
+
145
149
  if (!options.parseOnly && !options.parseCdl)
146
150
  all = all.then( readDependencies );
151
+
147
152
  return all.then( () => {
148
- moduleLayers.setLayers( a.sources );
153
+ moduleLayers.setLayers( input.sources );
149
154
  return compileDoX( model );
150
155
  });
151
156
 
152
157
  // Read file `filename` and parse its content, return messages
153
158
  async function readAndParse( filename ) {
154
- const { sources } = a;
159
+ const { sources } = input;
155
160
  if ( filename === false ) // module which has not been found
156
161
  return [];
157
162
  const rel = sources[filename] || path.relative( dir, filename );
@@ -173,9 +178,9 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
173
178
 
174
179
  // Combine the parse results (if there are not file IO errors)
175
180
  function testInvocation( values ) {
176
- if (a.repeated.length)
181
+ if (input.repeated.length)
177
182
  // repeated file names in invocation => just report these
178
- return Promise.reject( new InvocationError(a.repeated) );
183
+ return Promise.reject( new InvocationError(input.repeated) );
179
184
  return values;
180
185
  }
181
186
 
@@ -225,7 +230,7 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
225
230
  // A non-proper dictionary (i.e. with prototype) is safe if the keys are
226
231
  // absolute file names - they start with `/` or `\` or similar
227
232
  dir = path.resolve(dir);
228
- const a = processFilenames( filenames, dir );
233
+ const a = processFilenamesSync( filenames, dir );
229
234
 
230
235
  const model = { sources: a.sources, options };
231
236
  model.$messageFunctions = createMessageFunctions( options, 'compile', model );
@@ -453,22 +458,44 @@ function compileDoX( model ) {
453
458
  return propagator.propagate( model );
454
459
  }
455
460
 
456
- // Process an array of `filenames`. Returns an object with properties:
457
- // - `sources`: dictionary which has a filename as key (value is irrelevant)
458
- // - `files`: the argument array without repeating the same name
459
- // - `repeated`: array of filenames which have been repeatedly listed
460
- // (listed only once here even if listed thrice)
461
- //
462
- // Note: there is nothing file-specific about the filenames, the filenames are
463
- // not normalized - any strings work
464
- function processFilenames( filenames, dir ) {
465
- const sources = Object.create(null); // not {} = no [[Prototype]]
466
- const files = [];
467
- const repeated = [];
461
+ /**
462
+ * Process an array of `filenames`. Returns an object with properties:
463
+ * - `sources`: dictionary which has a filename as key (value is irrelevant)
464
+ * - `files`: the argument array without repeating the same name
465
+ * - `repeated`: array of filenames which have been repeatedly listed
466
+ * (listed only once here even if listed thrice)
467
+ *
468
+ * Note: there is nothing file-specific about the filenames, the filenames are
469
+ * not normalized - any strings work
470
+ */
471
+ async function processFilenames( filenames, dir ) {
472
+ const filenameMap = Object.create(null);
468
473
 
474
+ const promises = [];
469
475
  for (const originalName of filenames) {
470
- let name = path.resolve(dir, originalName);
476
+ const setName = (name) => {
477
+ filenameMap[originalName] = name;
478
+ };
479
+ // Resolve possible symbolic link; if the file does not exist
480
+ // we just continue using the original name because readFile()
481
+ // already handles non-existent files.
482
+ const promise = fs.promises.realpath(path.resolve(dir, originalName))
483
+ .then(setName, () => setName(originalName));
484
+ promises.push(promise);
485
+ }
471
486
 
487
+ await Promise.all(promises);
488
+ return createSourcesDict( filenames, filenameMap, dir );
489
+ }
490
+
491
+ /**
492
+ * Synchronous version of processFilenames().
493
+ */
494
+ function processFilenamesSync( filenames, dir ) {
495
+ const filenameMap = Object.create(null);
496
+
497
+ for (const originalName of filenames) {
498
+ let name = path.resolve(dir, originalName);
472
499
  try {
473
500
  // Resolve possible symbolic link; if the file does not exist
474
501
  // we just continue using the original name because readFile()
@@ -478,7 +505,29 @@ function processFilenames( filenames, dir ) {
478
505
  catch (e) {
479
506
  // Ignore the not-found (ENOENT) error
480
507
  }
508
+ filenameMap[originalName] = name;
509
+ }
510
+
511
+ return createSourcesDict( filenames, filenameMap, dir );
512
+ }
513
+
514
+ /**
515
+ * Creates the sources dictionary as well as a list of absolute filenames.
516
+ * If files are repeated, `repeated` will contain ArgumentErrors for it.
517
+ *
518
+ * @param {string[]} filenames List of (possibly relative) filenames. Defines the file order.
519
+ * @param {Record<string, string>} filenameMap Map from original name to actual filename
520
+ * (e.g. from symlink to underlying path)
521
+ * @param {string} dir "Current working directory"
522
+ * @return {{sources: object, files: string[], repeated: ArgumentError[]}}
523
+ */
524
+ function createSourcesDict( filenames, filenameMap, dir ) {
525
+ const sources = Object.create(null);
526
+ const files = [];
527
+ const repeated = [];
481
528
 
529
+ for (const originalName of filenames) {
530
+ const name = filenameMap[originalName];
482
531
  if (!sources[name]) {
483
532
  sources[name] = path.relative( dir, name );
484
533
  files.push(name);
@@ -488,10 +537,10 @@ function processFilenames( filenames, dir ) {
488
537
  repeated.push( new ArgumentError( name, msg ) );
489
538
  }
490
539
  }
540
+
491
541
  return { sources, files, repeated };
492
542
  }
493
543
 
494
-
495
544
  module.exports = {
496
545
  parseX,
497
546
  compileX,
@@ -5,9 +5,9 @@
5
5
  // used when resolving element references: when starting a references at a
6
6
  // certain definition or element, which names are allowed next?
7
7
  //
8
- // To calculate that info, the compiler might needs the same info for other
9
- // definitions. In other words: it calls itself recursively (using an iterative
10
- // algorithm where appropriate). The be able to calculate that info on demand,
8
+ // To calculate that info, the compiler might need the same info for other
9
+ // definitions. In other words: it calls itself recursively (using an iterative
10
+ // algorithm where appropriate). To be able to calculate that info on demand,
11
11
  // the definitions need to have enough information, which must have been set in
12
12
  // an earlier compiler phase. It is essential to do things in the right order.
13
13
 
@@ -109,6 +109,8 @@ function populate( model ) {
109
109
  function traverseElementEnvironments( art ) {
110
110
  populateView( art );
111
111
  environment( art );
112
+ if (art.elements$)
113
+ mergeSpecifiedElements(art);
112
114
  forEachMember( art, traverseElementEnvironments );
113
115
  }
114
116
 
@@ -166,7 +168,7 @@ function populate( model ) {
166
168
  const chain = [];
167
169
  while (art && !('_effectiveType' in art) &&
168
170
  (art.type || art._origin || art.value && art.value.path) &&
169
- // TODO: really stop at art.enum?
171
+ // TODO: really stop at art.enum? See #8942
170
172
  !art.target && !art.enum && !art.elements && !art.items) {
171
173
  chain.push( art );
172
174
  setLink( art, '_effectiveType', 0 ); // initial setting in case of cycles
@@ -425,8 +427,6 @@ function populate( model ) {
425
427
  setLink( view, '_status', '_query' );
426
428
  // must be run in order “sub query in FROM first”:
427
429
  traverseQueryPost( view.query, null, populateQuery );
428
- if (view.elements$) // specified elements
429
- mergeSpecifiedElements( view );
430
430
  if (!view.$entity) {
431
431
  model._entities.push( view );
432
432
  view.$entity = ++model.$entity;
@@ -435,14 +435,25 @@ function populate( model ) {
435
435
  }
436
436
  }
437
437
 
438
- function mergeSpecifiedElements( view ) {
438
+ /**
439
+ * Merge _specified_ elements with _inferred_ elements in the given view/element,
440
+ * where specified elements can appear through CSN.
441
+ *
442
+ * We only copy annotations, since they are not part of `columns`,
443
+ * but only appear in `elements` in CSN.
444
+ *
445
+ * This is important to ensure re-compilability.
446
+ *
447
+ * @param art
448
+ */
449
+ function mergeSpecifiedElements( art ) {
439
450
  // Later we use specified elements as proxies to inferred of leading query
440
451
  // (No, we probably do not.)
441
- for (const id in view.elements) {
442
- const ielem = view.elements[id]; // inferred element
443
- const selem = view.elements$[id]; // specified element
452
+ for (const id in art.elements) {
453
+ const ielem = art.elements[id]; // inferred element
454
+ const selem = art.elements$[id]; // specified element
444
455
  if (!selem) {
445
- info( 'query-missing-element', [ ielem.name.location, view ], { id },
456
+ info( 'query-missing-element', [ ielem.name.location, art ], { id },
446
457
  'Element $(ID) is missing in specified elements' );
447
458
  }
448
459
  else {
@@ -452,13 +463,20 @@ function populate( model ) {
452
463
  ielem[prop] = selem[prop];
453
464
  }
454
465
  selem.$replacement = true;
466
+ if (selem.elements) {
467
+ setLink(ielem, 'elements$', selem.elements);
468
+ delete selem.elements;
469
+ }
455
470
  }
456
471
  }
457
- for (const id in view.elements$) {
458
- const selem = view.elements$[id]; // specified element
459
- if (!selem.$replacement) {
460
- error( 'query-unspecified-element', [ selem.name.location, selem ], { id },
461
- 'Element $(ID) does not result from the query' );
472
+ // Without element expansion, we can't merge nested elements.
473
+ if (art.kind === 'entity' || enableExpandElements) {
474
+ for (const id in art.elements$) {
475
+ const selem = art.elements$[id]; // specified element
476
+ if (!selem.$replacement) {
477
+ error( 'query-unspecified-element', [ selem.name.location, selem ], { id },
478
+ 'Element $(ID) does not result from the query' );
479
+ }
462
480
  }
463
481
  }
464
482
  }
@@ -564,7 +582,8 @@ function populate( model ) {
564
582
  error( null, [ col.location, query ], { prop: (col.expand ? 'expand' : 'inline') },
565
583
  'Unsupported nested $(PROP)' );
566
584
  }
567
- if (!col.value && !col.expand)
585
+ // If neither expression (value), expand nor new association.
586
+ if (!col.value && !col.expand && !(col.target && col.type))
568
587
  continue; // error should have been reported by parser
569
588
  if (col.inline) {
570
589
  col.kind = '$inline';
@@ -117,6 +117,9 @@ function propagate( model ) {
117
117
  function runMembers( art ) {
118
118
  // console.log('MEMBERS:',refString(art), art.elements ? Object.keys(art.elements) : 0)
119
119
  forEachMember( art, run ); // after propagation in parent!
120
+ // propagate to sub query elements even if not requested:
121
+ if (art.$queries)
122
+ art.$queries.forEach( run );
120
123
  let obj = art;
121
124
  if (art.returns) {
122
125
  obj = art.returns;
@@ -230,7 +233,7 @@ function propagate( model ) {
230
233
  }
231
234
 
232
235
  function notWithExpand( prop, target, source ) {
233
- if (!target.expand)
236
+ if (!target.expand || prop === 'type' && source.elements)
234
237
  always( prop, target, source );
235
238
  }
236
239
 
@@ -50,6 +50,7 @@ const { dictLocation } = require('../base/location');
50
50
  const { searchName, weakLocation } = require('../base/messages');
51
51
  const { combinedLocation } = require('../base/location');
52
52
  const { forEachValue } = require('../utils/objectUtils');
53
+ const { typeParameters } = require('./builtins');
53
54
 
54
55
  const { kindProperties } = require('./base');
55
56
  const {
@@ -89,13 +90,13 @@ function resolve( model ) {
89
90
  } = model.$messageFunctions;
90
91
  const {
91
92
  resolvePath,
92
- resolveTypeArguments,
93
93
  defineAnnotations,
94
94
  attachAndEmitValidNames,
95
95
  lateExtensions,
96
96
  effectiveType,
97
97
  directType,
98
98
  resolveType,
99
+ resolveTypeArgumentsUnchecked,
99
100
  populateQuery,
100
101
  } = model.$functions;
101
102
  const { environment } = model.$volatileFunctions;
@@ -371,8 +372,8 @@ function resolve( model ) {
371
372
  // console.log(obj.name,obj._origin.name)
372
373
  if (obj._origin && obj._origin.$inferred === 'REDIRECTED')
373
374
  resolveTarget( art, obj._origin );
374
- // console.log(message( null, obj.location, obj, {target:obj.target}, 'Info','TARGET')
375
- // .toString(), obj.target.$inferred)
375
+ // console.log(error( 'test-target', [ obj.location, obj ],
376
+ // { target: obj.target, kind: obj.kind }, 'Target: $(TARGET), Kind $(KIND)'));
376
377
  if (!obj.target.$inferred || obj.target.$inferred === 'aspect-composition')
377
378
  resolveTarget( art, obj );
378
379
  else
@@ -448,8 +449,10 @@ function resolve( model ) {
448
449
  // propagation/rewrite has been done yet, cyclic dependency must have been
449
450
  // checked before!
450
451
  function getAssocSpec( type ) {
452
+ const cyclic = new Set(); // TODO(#8942): May not be necessary if effectiveType() is adapted.
451
453
  // only to be called without cycles
452
- while (type) {
454
+ while (type && !cyclic.has(type)) {
455
+ cyclic.add(type);
453
456
  if (type.on || type.foreignKeys || type.targetAspect)
454
457
  return type;
455
458
  type = directType( type );
@@ -464,7 +467,7 @@ function resolve( model ) {
464
467
  // op.val is also correctly set with CSN input
465
468
  elem.type = { ...type, $inferred: 'cast' };
466
469
  setArtifactLink( elem.type, type._artifact );
467
- for (const prop of [ 'length', 'precision', 'scale', 'srid' ]) {
470
+ for (const prop of typeParameters.list) {
468
471
  if (elem.value[prop])
469
472
  elem[prop] = { ...elem.value[prop], $inferred: 'cast' };
470
473
  }
@@ -1026,6 +1029,15 @@ function resolve( model ) {
1026
1029
  return;
1027
1030
  }
1028
1031
  const target = resolvePath( obj.target, 'target', art );
1032
+
1033
+ if (obj._pathHead && obj.type && !obj.type.$inferred && art._main && art._main.query) {
1034
+ // New association inside expand/inline: The on-condition can't be properly checked,
1035
+ // so abort early. See #8797
1036
+ error( 'query-unexpected-assoc', [ obj.name.location, art ], {},
1037
+ 'Unexpected new association in expand/inline' );
1038
+ return; // avoid subsequent errors
1039
+ }
1040
+
1029
1041
  if (obj.on) {
1030
1042
  if (!art._main || !art._parent.elements && !art._parent.items && !art._parent.targetAspect) {
1031
1043
  // TODO: test of .items a bit unclear - we should somehow restrict the
@@ -1057,20 +1069,29 @@ function resolve( model ) {
1057
1069
  else if (art.kind === 'mixin') {
1058
1070
  error( 'assoc-in-mixin', [ obj.target.location, art ], {},
1059
1071
  'Managed associations are not allowed for MIXIN elements' );
1072
+ return; // avoid subsequent errors
1073
+ }
1074
+ else if (obj.type && !obj.type.$inferred && art._parent && art._parent.kind === 'select') {
1075
+ // New association in views, i.e. parent is a query.
1076
+ error( 'query-expected-on-condition', [ obj.target.location, art ], {},
1077
+ 'Expected on-condition for published association' );
1078
+ return; // avoid subsequent errors
1060
1079
  }
1061
1080
  else if (target && !obj.foreignKeys && target.kind === 'entity') {
1062
1081
  if (obj.$inferred === 'REDIRECTED') {
1063
1082
  addImplicitForeignKeys( art, obj, target );
1064
1083
  }
1065
- else if (!obj.type || obj.type.$inferred || obj.target.$inferred) { // REDIRECTED
1066
- resolveRedirected( art, target );
1067
- }
1068
- else if (obj.type._artifact && obj.type._artifact.internal) { // cds.Association, ...
1084
+ else if (obj.type && obj.type._artifact && obj.type._artifact.internal) {
1085
+ // cds.Association, ...
1069
1086
  addImplicitForeignKeys( art, obj, target );
1070
1087
  }
1071
- // else console.log( message( null,obj.location,obj, {target}, 'Info','NOTARGET').toString())
1072
1088
  }
1073
- // else console.log( message( null, obj.location, obj, {target}, 'Info','NORE').toString())
1089
+
1090
+ if (target && !target.$inferred) {
1091
+ if (!obj.type || obj.type.$inferred || obj.target.$inferred) { // REDIRECTED
1092
+ resolveRedirected( art, target );
1093
+ }
1094
+ }
1074
1095
  }
1075
1096
 
1076
1097
  function addImplicitForeignKeys( art, obj, target ) {
@@ -1130,7 +1151,9 @@ function resolve( model ) {
1130
1151
  const assoc = directType( elem );
1131
1152
  const origType = assoc && effectiveType( assoc );
1132
1153
  if (!origType || !origType.target) {
1133
- error( 'redirected-no-assoc', [ elem.target.location, elem ], {},
1154
+ const path = (elem.value && elem.value.path);
1155
+ const loc = (path && path[path.length - 1] || elem.value || elem).location;
1156
+ error( 'redirected-no-assoc', [ loc, elem ], {},
1134
1157
  'Only an association can be redirected' );
1135
1158
  return;
1136
1159
  }
@@ -1138,7 +1161,7 @@ function resolve( model ) {
1138
1161
  // .toString(), elem.value)
1139
1162
  const nav = elem._main && elem._main.query && elem.value && pathNavigation( elem.value );
1140
1163
  if (nav && nav.item !== elem.value.path[elem.value.path.length - 1]) {
1141
- if (origType.on) {
1164
+ if (!elem.on && origType.on) {
1142
1165
  error( 'rewrite-not-supported', [ elem.target.location, elem ], {},
1143
1166
  // TODO: Better text ?
1144
1167
  'The ON condition is not rewritten here - provide an explicit ON condition' );
@@ -1151,7 +1174,9 @@ function resolve( model ) {
1151
1174
 
1152
1175
  const chain = [];
1153
1176
  if (target === origTarget) {
1154
- if (!elem.target.$inferred) {
1177
+ if (!elem.target.$inferred && !elem.on && !elem.foreignKeys) {
1178
+ // Only a managed redirection gets this info message. Because otherwise
1179
+ // we'd have to check whether on-condition/foreignKeys are the same.
1155
1180
  info( 'redirected-to-same', [ elem.target.location, elem ], { art: target },
1156
1181
  'The redirected target is the original $(ART)' );
1157
1182
  }
@@ -1265,8 +1290,71 @@ function resolve( model ) {
1265
1290
  // Resolve the type and its arguments if applicable.
1266
1291
  function resolveTypeExpr( art, user ) {
1267
1292
  const typeArt = resolveType( art.type, user );
1268
- if (typeArt)
1269
- resolveTypeArguments( art, typeArt, user );
1293
+ if (typeArt) {
1294
+ resolveTypeArgumentsUnchecked( art, typeArt, user );
1295
+ checkTypeArguments( art );
1296
+ }
1297
+ }
1298
+
1299
+ /**
1300
+ * Check the type arguments on `artWithType`.
1301
+ * If the effective type is an array or structured type, an error is emitted.
1302
+ */
1303
+ function checkTypeArguments( artWithType ) {
1304
+ // Note: `_effectiveType` may point to `artWithType` itself, if the type is structured.
1305
+ // Also: For enums, it points to the enum type, which is why this trick is needed.
1306
+ // TODO(#8942): May not be necessary if effectiveType() is adapted. Furthermore, the enum
1307
+ // trick may be removed if effectiveType() does not stop at enums.
1308
+ const cyclic = new Set();
1309
+ let effectiveTypeArt = effectiveType( artWithType );
1310
+ while (effectiveTypeArt && effectiveTypeArt.enum && !cyclic.has(effectiveTypeArt)) {
1311
+ cyclic.add(effectiveTypeArt);
1312
+ const underlyingEnumType = directType(effectiveTypeArt);
1313
+ if (underlyingEnumType)
1314
+ effectiveTypeArt = effectiveType(underlyingEnumType);
1315
+ else
1316
+ break;
1317
+ }
1318
+
1319
+ if (!effectiveTypeArt)
1320
+ return; // e.g. illegal definition references
1321
+
1322
+ const params = effectiveTypeArt.parameters &&
1323
+ effectiveTypeArt.parameters.map(p => p.name || p) || [];
1324
+
1325
+ for (const param of typeParameters.list) {
1326
+ if (artWithType[param] !== undefined) {
1327
+ if (!params.includes(param)) {
1328
+ // Whether the type ref itself is a builtin or a custom type with a builtin as base.
1329
+ const type = directType(artWithType);
1330
+
1331
+ let variant;
1332
+ if (type.builtin)
1333
+ // `.type` is already a builtin: use a nicer message.
1334
+ variant = 'builtin';
1335
+ else if (effectiveTypeArt.builtin)
1336
+ // base type is a builtin, i.e. a scalar
1337
+ variant = 'type';
1338
+ else
1339
+ // effectiveType is not a builtin -> array or structured
1340
+ variant = 'non-scalar';
1341
+
1342
+ error('type-unexpected-argument', [ artWithType[param].location, artWithType ], {
1343
+ '#': variant, prop: param, art: artWithType.type, type: effectiveTypeArt,
1344
+ });
1345
+ break; // Avoid spam: Only emit the first error.
1346
+ }
1347
+ else if (!typeParameters.expectedLiteralsFor[param].includes(artWithType[param].literal)) {
1348
+ error('type-unexpected-argument', [ artWithType[param].location, artWithType ], {
1349
+ '#': 'incorrect-type',
1350
+ prop: param,
1351
+ code: artWithType[param].literal,
1352
+ names: typeParameters.expectedLiteralsFor[param],
1353
+ });
1354
+ break; // Avoid spam: Only emit the first error.
1355
+ }
1356
+ }
1357
+ }
1270
1358
  }
1271
1359
 
1272
1360
  function resolveExpr( expr, expected, user, extDict, expandOrInline) {
@@ -157,8 +157,8 @@ function fns( model ) {
157
157
  const VolatileFns = model.$volatileFunctions;
158
158
  Object.assign( model.$functions, {
159
159
  resolveUncheckedPath,
160
+ resolveTypeArgumentsUnchecked,
160
161
  resolvePath,
161
- resolveTypeArguments,
162
162
  defineAnnotations,
163
163
  attachAndEmitValidNames,
164
164
  } );
@@ -459,35 +459,64 @@ function fns( model ) {
459
459
  }
460
460
  }
461
461
 
462
- // Resolve the type arguments provided with a type referenced for artifact or
463
- // element `artifact`. This function does nothing if the referred type
464
- // `typeArtifact` does not have a `parameters` property (currently, only
465
- // builtin-types have it, see ./builtins.js).
466
- //
467
- // For each property name `<prop>` in `typeArtifact.parameters`, we move a number
468
- // in art.$typeArgs (a vector of numbers with locations) to `artifact.<prop>`.
469
- // TODO: error if no parameters applicable
470
- // TODO: also check for number
471
- function resolveTypeArguments(artifact, typeArtifact, user) {
472
- const args = artifact.$typeArgs || [];
462
+ /**
463
+ * Resolve the type arguments of `artifact` according to the type `typeArtifact`.
464
+ * User is used for semantic message location.
465
+ *
466
+ * For builtins, for each property name `<prop>` in `typeArtifact.parameters`, we move a value
467
+ * in art.$typeArgs (a vector of numbers with locations) to `artifact.<prop>`.
468
+ *
469
+ * For non-builtins, we take either one or two arguments and interpret them
470
+ * as `length` or `precision`/`scale`.
471
+ *
472
+ * Left-over arguments are errors for non-builtins and warnings for builtins.
473
+ *
474
+ * @param {object} artifact
475
+ * @param {object} typeArtifact
476
+ * @param {CSN.Artifact} user
477
+ */
478
+ function resolveTypeArgumentsUnchecked(artifact, typeArtifact, user) {
479
+ let args = artifact.$typeArgs || [];
473
480
  const parameters = typeArtifact.parameters || [];
474
- const parLength = parameters.length;
475
481
 
476
- for (let i = 0; i < parLength; ++i) {
477
- let par = parameters[i];
478
- if (!(par instanceof Object))
479
- par = { name: par };
480
- if (!artifact[par.name] && i < args.length)
481
- artifact[par.name] = args[i];
482
+ if (parameters.length > 0) {
483
+ // For Builtins
484
+ for (let i = 0; i < parameters.length; ++i) {
485
+ let par = parameters[i];
486
+ if (!(par instanceof Object))
487
+ par = { name: par };
488
+ if (!artifact[par.name] && i < args.length)
489
+ artifact[par.name] = args[i];
490
+ }
491
+ args = args.slice(parameters.length);
482
492
  }
483
- if (args.length > parLength) {
484
- artifact.$typeArgs = artifact.$typeArgs.slice(parLength);
485
- warning( 'unexpected-type-arg', [ artifact.$typeArgs[0].location, user ],
486
- { art: typeArtifact }, 'Too many arguments for type $(ART)' );
493
+ else if (args.length > 0 && !typeArtifact.builtin) {
494
+ // One or two arguments are interpreted as either length or precision/scale.
495
+ // For builtins, we know what arguments are expected, and we do not need this mapping.
496
+ // Also, we expect non-structured types.
497
+ if (args.length === 1) {
498
+ artifact.length = args[0];
499
+ args = args.slice(1);
500
+ }
501
+ else if (args.length === 2) {
502
+ artifact.precision = args[0];
503
+ artifact.scale = args[1];
504
+ args = args.slice(2);
505
+ }
487
506
  }
488
- else if (artifact.$typeArgs) {
489
- delete artifact.$typeArgs;
507
+
508
+ if (!artifact.$typeArgs)
509
+ return;
510
+
511
+ // Warn about left-over arguments.
512
+ if (args.length > 0) {
513
+ const loc = [ args[args.length - 1].location, user ];
514
+ if (typeArtifact.builtin)
515
+ warning( 'type-ignoring-argument', loc, { art: typeArtifact } );
516
+ else
517
+ error( 'type-unexpected-argument', loc, { '#': 'std', art: typeArtifact });
490
518
  }
519
+ artifact.$typeArgs = undefined;
491
520
  }
492
521
 
493
522
  // Return artifact or element referred by name `head`. The first environment
@@ -732,7 +761,7 @@ function fns( model ) {
732
761
  signalNotFound( spec.undefinedDef || 'ref-undefined-def', [ item.location, user ],
733
762
  [ env ], { art: a } );
734
763
  }
735
- else if (art.name.select && art.name.select > 1) {
764
+ else if (art.name && art.name.select && art.name.select > 1) {
736
765
  // TODO: 'The current query has no element $(MEMBER)' with $self.MEMBER
737
766
  // and 'The sub query for alias $(ALIAS) has no element $(MEMBER)'
738
767
  // TODO: probably not extra messageId, but text variant
@@ -748,8 +777,13 @@ function fns( model ) {
748
777
  { param: 'Entity $(ART) has no parameter $(MEMBER)' } );
749
778
  }
750
779
  else {
780
+ const variant = art.kind === 'aspect' && !art.name && 'aspect';
751
781
  signalNotFound( spec.undefinedDef || 'ref-undefined-element', [ item.location, user ],
752
- [ env ], { art: searchName( art, item.id, 'element' ) } );
782
+ [ env ], {
783
+ '#': variant,
784
+ art: (variant ? '' : searchName( art, item.id, 'element' )),
785
+ id: item.id,
786
+ } );
753
787
  }
754
788
  return null;
755
789
  }
@@ -459,7 +459,13 @@ function tweakAssocs( model ) {
459
459
  if (!forKeys)
460
460
  break;
461
461
  setArtifactLink( item, null );
462
- error( 'rewrite-undefined-key', [ weakLocation( (elem.target || elem).location ), assoc ],
462
+ const culprit = elem.target && !elem.target.$inferred && elem.target ||
463
+ (elem.value && elem.value.path &&
464
+ elem.value.path[elem.value.path.length - 1]) ||
465
+ elem;
466
+ // TODO: probably better to collect the non-projected foreign keys
467
+ // and have one message for all
468
+ error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ],
463
469
  { id: item.id, art: alias._main },
464
470
  'Foreign key $(ID) has not been found in target $(ART)' );
465
471
  return null;