@sap/cds-compiler 3.5.4 → 3.6.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 (83) hide show
  1. package/CHANGELOG.md +56 -2
  2. package/bin/cdsc.js +14 -6
  3. package/doc/CHANGELOG_ARCHIVE.md +10 -10
  4. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  5. package/lib/api/main.js +32 -55
  6. package/lib/api/validate.js +5 -0
  7. package/lib/base/message-registry.js +104 -32
  8. package/lib/base/messages.js +277 -212
  9. package/lib/base/optionProcessorHelper.js +9 -2
  10. package/lib/base/shuffle.js +50 -0
  11. package/lib/checks/actionsFunctions.js +37 -20
  12. package/lib/checks/foreignKeys.js +13 -6
  13. package/lib/checks/nonexpandableStructured.js +1 -2
  14. package/lib/checks/onConditions.js +21 -19
  15. package/lib/checks/parameters.js +1 -1
  16. package/lib/checks/queryNoDbArtifacts.js +2 -0
  17. package/lib/checks/types.js +16 -22
  18. package/lib/compiler/assert-consistency.js +31 -28
  19. package/lib/compiler/builtins.js +20 -4
  20. package/lib/compiler/checks.js +72 -63
  21. package/lib/compiler/define.js +396 -314
  22. package/lib/compiler/extend.js +55 -49
  23. package/lib/compiler/index.js +5 -0
  24. package/lib/compiler/populate.js +28 -11
  25. package/lib/compiler/propagator.js +2 -1
  26. package/lib/compiler/resolve.js +28 -13
  27. package/lib/compiler/shared.js +15 -10
  28. package/lib/compiler/utils.js +7 -7
  29. package/lib/edm/annotations/genericTranslation.js +51 -46
  30. package/lib/edm/annotations/preprocessAnnotations.js +37 -40
  31. package/lib/edm/csn2edm.js +69 -21
  32. package/lib/edm/edm.js +2 -2
  33. package/lib/edm/edmInboundChecks.js +6 -8
  34. package/lib/edm/edmPreprocessor.js +88 -80
  35. package/lib/edm/edmUtils.js +6 -15
  36. package/lib/gen/Dictionary.json +81 -13
  37. package/lib/gen/language.checksum +1 -1
  38. package/lib/gen/language.interp +2 -1
  39. package/lib/gen/languageParser.js +4680 -4484
  40. package/lib/inspect/inspectModelStatistics.js +2 -1
  41. package/lib/inspect/inspectPropagation.js +2 -1
  42. package/lib/json/from-csn.js +131 -78
  43. package/lib/json/to-csn.js +39 -23
  44. package/lib/language/antlrParser.js +0 -3
  45. package/lib/language/docCommentParser.js +7 -3
  46. package/lib/language/errorStrategy.js +3 -2
  47. package/lib/language/genericAntlrParser.js +96 -41
  48. package/lib/language/language.g4 +112 -128
  49. package/lib/language/multiLineStringParser.js +2 -1
  50. package/lib/main.d.ts +115 -2
  51. package/lib/main.js +16 -3
  52. package/lib/model/csnRefs.js +3 -3
  53. package/lib/model/csnUtils.js +109 -179
  54. package/lib/model/enrichCsn.js +13 -8
  55. package/lib/model/revealInternalProperties.js +4 -3
  56. package/lib/optionProcessor.js +19 -3
  57. package/lib/render/manageConstraints.js +11 -15
  58. package/lib/render/toCdl.js +144 -47
  59. package/lib/render/toHdbcds.js +22 -22
  60. package/lib/render/toRename.js +3 -4
  61. package/lib/render/toSql.js +29 -20
  62. package/lib/render/utils/delta.js +3 -1
  63. package/lib/render/utils/sql.js +2 -14
  64. package/lib/transform/db/associations.js +6 -6
  65. package/lib/transform/db/cdsPersistence.js +3 -3
  66. package/lib/transform/db/constraints.js +4 -6
  67. package/lib/transform/db/expansion.js +4 -4
  68. package/lib/transform/db/flattening.js +12 -15
  69. package/lib/transform/db/temporal.js +4 -3
  70. package/lib/transform/db/transformExists.js +2 -1
  71. package/lib/transform/draft/db.js +7 -7
  72. package/lib/transform/forOdataNew.js +15 -4
  73. package/lib/transform/forRelationalDB.js +53 -39
  74. package/lib/transform/odata/toFinalBaseType.js +106 -82
  75. package/lib/transform/odata/typesExposure.js +26 -17
  76. package/lib/transform/odata/utils.js +1 -1
  77. package/lib/transform/parseExpr.js +1 -1
  78. package/lib/transform/transformUtilsNew.js +33 -10
  79. package/lib/transform/translateAssocsToJoins.js +8 -7
  80. package/lib/transform/universalCsn/coreComputed.js +7 -5
  81. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -4
  82. package/lib/utils/timetrace.js +2 -2
  83. package/package.json +1 -2
@@ -14,6 +14,7 @@
14
14
  const { locationString } = require('../base/messages');
15
15
  const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
16
16
  const { pathName } = require('../compiler/utils');
17
+ const { CompilerAssertion } = require('../base/error');
17
18
 
18
19
  const compilerVersion = require('../../package.json').version;
19
20
  const creator = `CDS Compiler v${ compilerVersion }`;
@@ -106,7 +107,7 @@ const transformers = {
106
107
  notNull: value,
107
108
  default: expression,
108
109
  // targetElement: ignore, // special display of foreign key, renameTo: select
109
- value: enumValue, // do not list for select items as elements
110
+ value: enumValueOrCalc, // do not list for select items as elements
110
111
  query,
111
112
  elements,
112
113
  actions, // TODO: just normal dictionary
@@ -570,7 +571,7 @@ function standard( node ) {
570
571
  function unexpected( val, csn, node, prop ) {
571
572
  if (strictMode) {
572
573
  const loc = val && val.location || node.location;
573
- throw new Error( `Unexpected property ${ prop } in ${ locationString(loc) }` );
574
+ throw new CompilerAssertion( `Unexpected property ${ prop } in ${ locationString(loc) }` );
574
575
  }
575
576
  // otherwise, just ignore the unexpected property
576
577
  }
@@ -820,7 +821,7 @@ function returns( art, csn, _node, prop ) {
820
821
  }
821
822
 
822
823
  function definition( art, _csn, _node, prop ) {
823
- if (!art || typeof art !== 'object')
824
+ if (!art || typeof art !== 'object' || art.builtin)
824
825
  return undefined; // TODO: complain with strict
825
826
  // Do not include namespace definitions or inferred construct (in gensrc):
826
827
  if (art.kind === 'namespace' || art.$inferred && gensrcFlavor)
@@ -1049,6 +1050,11 @@ function hasExplicitProp( ref, alsoLikeExplicit ) {
1049
1050
  return ref && (!ref.$inferred || ref.$inferred === alsoLikeExplicit );
1050
1051
  }
1051
1052
 
1053
+ /**
1054
+ * @param art
1055
+ * @param user
1056
+ * @return {boolean|string[]}
1057
+ */
1052
1058
  function originRef( art, user ) {
1053
1059
  const r = [];
1054
1060
  // do not use name.element, as we might allow `.`s in name
@@ -1143,7 +1149,7 @@ function artifactRef( node, terse ) {
1143
1149
  const root = Array.isArray(link) ? link[0] : link;
1144
1150
  if (!root) { // XSN directly coming from the parser
1145
1151
  if (strictMode && node.scope === 'typeOf')
1146
- throw new Error( `Unexpected TYPE OF in ${ locationString(node.location) }`);
1152
+ throw new CompilerAssertion( `Unexpected TYPE OF in ${ locationString(node.location) }`);
1147
1153
  return renderArtifactPath( node, path, terse, node.scope );
1148
1154
  }
1149
1155
  const { absolute } = root.name;
@@ -1256,17 +1262,17 @@ function value( node ) {
1256
1262
  return r;
1257
1263
  }
1258
1264
 
1259
- function enumValue( v, csn, node ) {
1260
- // Enums can have values but if enums are extended, their kind is 'element',
1261
- // so we check whether the node is inside an extension. (TODO: still?)
1262
- if (universalCsn && v.$inferred)
1263
- return;
1264
- // (with gensrc, the symbol itself would not make it into the CSN)
1265
- if (node.kind === 'enum' || node._parent && node._parent.kind === 'extend')
1265
+ function enumValueOrCalc( v, csn, node ) {
1266
+ if (v.$inferred && (universalCsn || gensrcFlavor))
1267
+ return undefined;
1268
+ // Enums can have values but if enums are extended, their kind is 'element'
1269
+ if (node.kind === 'enum' || node.$syntax === 'enum')
1266
1270
  Object.assign( csn, expression( v ) );
1271
+ else if (node.$syntax === 'calc') // TODO: || node._parent?.kind === 'extend'
1272
+ return expression( v );
1273
+ return undefined;
1267
1274
  }
1268
1275
 
1269
-
1270
1276
  function onCondition( cond, csn, node ) {
1271
1277
  if (gensrcFlavor) {
1272
1278
  if (node._origin && node._origin.$inferred === 'REDIRECTED')
@@ -1280,14 +1286,14 @@ function onCondition( cond, csn, node ) {
1280
1286
  function condition( node ) {
1281
1287
  const expr = exprInternal( node, 'no' );
1282
1288
  return (Array.isArray( expr ))
1283
- ? flattenenInternalXpr( expr )
1289
+ ? flattenInternalXpr( expr, node.op?.val )
1284
1290
  : !expr.cast && !expr.func && expr.xpr || [ expr ];
1285
1291
  }
1286
1292
 
1287
1293
  function expression( node ) {
1288
1294
  const expr = exprInternal( node, 'no' );
1289
1295
  return (Array.isArray( expr ))
1290
- ? { xpr: flattenenInternalXpr( expr ) }
1296
+ ? { xpr: flattenInternalXpr( expr, node.op?.val ) }
1291
1297
  : expr;
1292
1298
  }
1293
1299
 
@@ -1331,8 +1337,9 @@ function exprInternal( node, xprParens ) {
1331
1337
  if (!node.op) // parse error
1332
1338
  return { xpr: [] };
1333
1339
 
1334
- const { val } = node.op;
1340
+ let { val } = node.op;
1335
1341
  switch (val) {
1342
+ case 'nary':
1336
1343
  case 'ixpr':
1337
1344
  case 'xpr':
1338
1345
  break;
@@ -1346,21 +1353,30 @@ function exprInternal( node, xprParens ) {
1346
1353
  const nary = [];
1347
1354
  for (const item of node.args)
1348
1355
  nary.push( { val, literal: 'token' }, item );
1356
+ val = 'nary';
1349
1357
  node = {
1350
- op: { val: 'ixpr' },
1351
- args: (nary.length > 2 ? nary.slice(1) : nary),
1358
+ op: { val },
1359
+ args: (nary.length > 2 ? nary.slice(1) : nary), // length 1,2 only with CSN v0
1352
1360
  $parens: node.$parens,
1353
1361
  };
1354
1362
  }
1355
1363
  }
1356
1364
  const rargs = node.args.map( exprInternal );
1357
1365
  if (val === 'xpr' || node.$parens)
1358
- return extra( { xpr: flattenenInternalXpr( rargs ) }, node, (xprParens === 'no' ? 0 : 1) );
1359
- return rargs.length === 1 ? rargs[0] : rargs;
1360
- }
1361
-
1362
- function flattenenInternalXpr( array ) {
1363
- return (structXpr) ? array : array.flat( Infinity );
1366
+ return extra( { xpr: flattenInternalXpr( rargs, val ) }, node, (xprParens === 'no' ? 0 : 1) );
1367
+ return rargs.length === 1 ? rargs[0] : flattenInternalXpr( rargs, val );
1368
+ }
1369
+
1370
+ function flattenInternalXpr( array, op ) {
1371
+ if (!structXpr)
1372
+ return array.flat( Infinity );
1373
+ if (array.length < 5 || op !== 'nary')
1374
+ return array;
1375
+ let left = array.slice( 0, 3 );
1376
+ let index = 3;
1377
+ while (index < array.length)
1378
+ left = [ left, array[index++], array[index++] ];
1379
+ return left;
1364
1380
  }
1365
1381
 
1366
1382
  function query( node, csn, xsn, _prop, expectedParens = 0 ) {
@@ -164,11 +164,8 @@ function parse( source, filename = '<undefined>.cds',
164
164
  if (options.docComment !== false) {
165
165
  for (const token of tokenStream.tokens) {
166
166
  if (token.type === parser.constructor.DocComment && !token.isUsed) {
167
- // TODO: think of 'syntax-unexpected-doc-comment'
168
167
  messageFunctions.info( 'syntax-ignoring-doc-comment', parser.tokenLocation(token), {},
169
168
  'Ignoring doc comment as it is not written at a defined position' );
170
- // this is also for position inside some artifact definition, i.e. previous text
171
- // "does not belong to any artifact" might be confusing
172
169
  }
173
170
  }
174
171
  }
@@ -6,6 +6,10 @@ const {
6
6
  cdlNewLineRegEx,
7
7
  } = require('./textUtils');
8
8
 
9
+ const fencedCommentRegEx = /^\s*[*]/;
10
+ const footerFenceRegEx = /\s*[*]+\/$/;
11
+ const hasContentOnFirstLineRegEx = /\/\*+\s*\S/;
12
+
9
13
  /**
10
14
  * Get the content of a JSDoc-like comment and remove all surrounding asterisks, etc.
11
15
  * If the comment only contains whitespace it is seen as empty and `null` is returned
@@ -36,7 +40,7 @@ function parseDocComment( comment ) {
36
40
 
37
41
  // If the comment already has content on the first line, i.e. after `/**`,
38
42
  // its leading whitespace is ignored for whitespace trimming.
39
- const hasContentOnFirstLine = /\/\*+\s*\S/.test(lines[0]);
43
+ const hasContentOnFirstLine = hasContentOnFirstLineRegEx.test(lines[0]);
40
44
 
41
45
  // First line, i.e. header, is always trimmed from left.
42
46
  lines[0] = removeHeaderFence(lines[0]).trimStart();
@@ -139,7 +143,7 @@ function removeHeaderFence( line ) {
139
143
  * @returns {string} header without fence
140
144
  */
141
145
  function removeFooterFence( line ) {
142
- return line.replace(/\s*[*]+\/$/, '');
146
+ return line.replace(footerFenceRegEx, '');
143
147
  }
144
148
 
145
149
  /**
@@ -151,7 +155,7 @@ function removeFooterFence( line ) {
151
155
  function isFencedComment( lines ) {
152
156
  const index = lines.findIndex((line, i) => {
153
157
  const exclude = (i === 0 || i === lines.length - 1);
154
- return !exclude && !(/^\s*[*]/.test(line));
158
+ return !exclude && !(fencedCommentRegEx.test(line));
155
159
  });
156
160
  return index === -1 && lines.length > 2;
157
161
  }
@@ -38,6 +38,7 @@ const {
38
38
  } = require('antlr4/src/antlr4/PredictionContext');
39
39
  const { ATNState } = require('antlr4/src/antlr4/atn/ATNState');
40
40
  const { IntervalSet, Interval } = require('antlr4/src/antlr4/IntervalSet');
41
+ const { CompilerAssertion } = require('../base/error');
41
42
 
42
43
  const keywordRegexp = /^[a-zA-Z]+$/; // we don't have keywords with underscore
43
44
 
@@ -317,7 +318,7 @@ function consumeUntil( recognizer, set ) {
317
318
  }
318
319
  // console.log('CONSUMED:',s,this.getTokenDisplay( recognizer.getCurrentToken(),
319
320
  // recognizer ),recognizer.getCurrentToken().line);
320
- // throw new Error('Sync')
321
+ // throw new CompilerAssertion('Sync')
321
322
  }
322
323
  }
323
324
 
@@ -471,7 +472,7 @@ function getExpectedTokensForMessage( recognizer, offendingToken, deadEnds ) {
471
472
  if (recognizer.state < 0)
472
473
  return [];
473
474
  if (recognizer.state >= atn.states.length) {
474
- throw new Error( `Invalid state number ${ recognizer.state } for ${
475
+ throw new CompilerAssertion( `Invalid state number ${ recognizer.state } for ${
475
476
  this.getTokenErrorDisplay( offendingToken ) }`);
476
477
  }
477
478
 
@@ -111,6 +111,10 @@ Object.assign(GenericAntlrParser.prototype, {
111
111
  finalizeDictOrArray,
112
112
  createPrefixOp,
113
113
  setMaxCardinality,
114
+ setNullability,
115
+ reportDuplicateClause,
116
+ reportUnexpectedExtension,
117
+ reportUnexpectedSpace,
114
118
  pushIdent,
115
119
  handleComposition,
116
120
  associationInSelectItem,
@@ -165,6 +169,7 @@ function noSemicolonHere() {
165
169
  // Using this function "during ATN decision making" has no effect
166
170
  // In front of an ATN decision, you might specify dedicated excludes
167
171
  // for non-LA1 tokens via a sub-array in excludes[0].
172
+ // TODO: consider $nextTokens…, see commented use in rule `elementProperties`
168
173
  function excludeExpected( excludes ) {
169
174
  if (excludes) {
170
175
  // @ts-ignore
@@ -192,8 +197,8 @@ function setLocalTokenIfBefore( string, tokenName, before, inSameLine ) {
192
197
  ll1.type = this.constructor[tokenName];
193
198
  }
194
199
 
195
- function setLocalTokenForId( tokenNameMap ) {
196
- const tokenName = tokenNameMap[this._input.LT(2).text || ''];
200
+ function setLocalTokenForId( offset, tokenNameMap ) {
201
+ const tokenName = tokenNameMap[this._input.LT( offset ).text.toUpperCase() || ''];
197
202
  const ll1 = this.getCurrentToken();
198
203
  if (tokenName &&
199
204
  (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )))
@@ -219,9 +224,9 @@ function setLocalTokenForId( tokenNameMap ) {
219
224
  function noAssignmentInSameLine() {
220
225
  const t = this.getCurrentToken();
221
226
  if (t.text === '@' && t.line <= this._input.LT(-1).line) {
222
- this.warning( 'syntax-missing-newline', t, { anno: '‹anno›' }, // TODO: single quotes, @()
227
+ this.warning( 'syntax-missing-semicolon', t, { code: ';' },
223
228
  // eslint-disable-next-line max-len
224
- 'Add a newline before $(ANNO) to indicate that it belongs to the next statement' );
229
+ 'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' );
225
230
  }
226
231
  }
227
232
 
@@ -319,7 +324,7 @@ function attachLocation( art ) {
319
324
  return art;
320
325
  }
321
326
 
322
- function assignAnnotation( art, anno, prefix = '', iHaveVariant = false ) {
327
+ function assignAnnotation( art, anno, prefix = '' ) {
323
328
  const { name, $flatten } = anno;
324
329
  const { path } = name;
325
330
  if (path.broken || !path[path.length - 1].id)
@@ -329,11 +334,9 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant = false ) {
329
334
  if (name.variant) {
330
335
  const variant = pathName( name.variant.path );
331
336
  absolute = `${ prefix }${ pathname }#${ variant }`;
332
- if (iHaveVariant) { // TODO: do we really care in the parser / core compiler?
333
- this.error( 'anno-duplicate-variant', [ name.variant.location ],
334
- {}, // TODO: params
335
- 'Annotation variant has been already provided' );
336
- }
337
+ // We do not care anymore whether we get a second '#' with flattening. This
338
+ // can be produced via CSN and with delimited ids anyway. If backends care,
339
+ // they need to have their own check.
337
340
  }
338
341
  else if (!prefix || pathname !== '$value') {
339
342
  absolute = `${ prefix }${ pathname }`;
@@ -343,7 +346,7 @@ function assignAnnotation( art, anno, prefix = '', iHaveVariant = false ) {
343
346
  }
344
347
  if ($flatten) {
345
348
  for (const a of $flatten)
346
- this.assignAnnotation( art, a, `${ absolute }.`, iHaveVariant || name.variant);
349
+ this.assignAnnotation( art, a, `${ absolute }.` );
347
350
  }
348
351
  else {
349
352
  name.absolute = absolute;
@@ -682,16 +685,22 @@ function identAst( token, category, noTokenTypeCheck = false ) {
682
685
 
683
686
  // only to be used in @after
684
687
  // TODO: remove compatible stuff (A2J/checks use op: 'and'/'=')
685
- function argsExpression( args, useFirstLocation, compatible = null ) {
688
+ function argsExpression( args, nary, location ) {
686
689
  // console.log('AE:',args);
687
690
  if (args.length === 1)
688
691
  return args[0];
689
- const location = useFirstLocation && args[0] && { ...args[0].location };
690
- const op = compatible && args.length === 3 && args[1].val === compatible && args[1];
691
- const expr = (op)
692
- ? { op: { val: op.val, location: op.location }, args: [ args[0], args[2] ], location }
693
- : { op: { val: 'ixpr', location: this.startLocation() }, args, location };
694
- return this.attachLocation( expr );
692
+ if (nary && args.length === 3 && args[1]?.val === nary) {
693
+ return this.attachLocation( {
694
+ op: { val: nary, location: args[1].location },
695
+ args: [ args[0], args[2] ],
696
+ location: undefined,
697
+ } );
698
+ }
699
+ const op = {
700
+ val: (nary && nary !== '=' ? 'nary' : 'ixpr'), // there is no n-ary in rule conditionTerm
701
+ location: this.startLocation(),
702
+ };
703
+ return this.attachLocation( { op, args, location: location && { ...location } } );
695
704
  }
696
705
 
697
706
  function pushXprToken( args ) {
@@ -738,7 +747,7 @@ function valuePathAst( ref ) {
738
747
  // If a '-' is directly before an unsigned number, consider it part of the number;
739
748
  // otherwise (including for '+'), represent it as extra unary prefix operator.
740
749
  function signedExpression( args, expr ) {
741
- // if (args.length !== 1) throw Error()
750
+ // if (args.length !== 1) throw new CompilerAssertion()
742
751
  const sign = args[0];
743
752
  const nval
744
753
  = (sign.val === '-' &&
@@ -778,7 +787,7 @@ function numberLiteral( token, sign, text = token.text ) {
778
787
  if (!Number.isSafeInteger(num)) {
779
788
  if (sign == null) {
780
789
  this.error( 'syntax-expecting-unsigned-int', token,
781
- { '#': !text.match(/^[0-9]*$/) ? 'normal' : 'unsafe' } );
790
+ { '#': !text.match(/^\d*$/) ? 'normal' : 'unsafe' } );
782
791
  }
783
792
 
784
793
  else if (text !== `${ num }`) {
@@ -845,26 +854,33 @@ function pushIdent( path, ident, prefix ) {
845
854
  path.push( ident );
846
855
  }
847
856
  else {
848
- const tokenLoc = this.tokenLocation( prefix );
849
- if (tokenLoc.endLine !== ident.location.line ||
850
- tokenLoc.endCol !== ident.location.col) {
851
- const wsLocation = {
852
- file: ident.location.file,
853
- line: tokenLoc.endLine, // !
854
- col: tokenLoc.endCol, // !
855
- endLine: ident.location.line,
856
- endCol: ident.location.col,
857
- };
858
- this.error( 'syntax-unexpected-space', wsLocation, {}, // TODO: really Error?
859
- 'Expected identifier after \'@\' but found whitespace' );
860
- }
861
- ident.location.line = tokenLoc.line;
862
- ident.location.col = tokenLoc.col;
857
+ const { location } = ident;
858
+ const prefixLoc = this.reportUnexpectedSpace( prefix, location );
859
+ location.line = prefixLoc.line;
860
+ location.col = prefixLoc.col;
863
861
  ident.id = prefix.text + ident.id;
864
862
  path.push( ident );
865
863
  }
866
864
  }
867
865
 
866
+ // For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths
867
+ function reportUnexpectedSpace( prefix, location = this.tokenLocation( this._input.LT(1) ) ) {
868
+ const prefixLoc = this.tokenLocation( prefix );
869
+ if (prefixLoc.endLine !== location.line ||
870
+ prefixLoc.endCol !== location.col) {
871
+ const wsLocation = {
872
+ file: location.file,
873
+ line: prefixLoc.endLine, // !
874
+ col: prefixLoc.endCol, // !
875
+ endLine: location.line,
876
+ endCol: location.col,
877
+ };
878
+ this.warning( 'syntax-unexpected-space', wsLocation, { op: prefix.text },
879
+ 'Delete the whitespace after $(OP)' );
880
+ }
881
+ return prefixLoc;
882
+ }
883
+
868
884
  // Add new definition `art` to dictionary property `env` of node `parent`.
869
885
  // Return `art`.
870
886
  //
@@ -1038,14 +1054,53 @@ function leftAssocBinaryOp( left, opToken, eToken, right, extraProp = 'quantifie
1038
1054
  return { op, args: [ left, right ], location: left.location };
1039
1055
  }
1040
1056
 
1041
- function setMaxCardinality( art, token, max ) {
1042
- const location = this.tokenLocation( token );
1043
- if (!art.cardinality) {
1044
- art.cardinality = { targetMax: Object.assign( { location }, max ), location };
1057
+ const maxCardinalityKeywords = { 1: 'one', '*': 'many' };
1058
+
1059
+ function setMaxCardinality( art, targetMax, token ) { // - val
1060
+ if (token)
1061
+ targetMax.location = this.tokenLocation( token );
1062
+ if (art.cardinality) {
1063
+ this.reportDuplicateClause( 'cardinality', targetMax, art.cardinality.targetMax,
1064
+ maxCardinalityKeywords );
1045
1065
  }
1046
1066
  else {
1047
- this.warning( 'syntax-duplicate-cardinality', location, { keyword: token.text },
1048
- 'The target cardinality has already been specified - ignoring $(KEYWORD)' );
1067
+ art.cardinality = { targetMax, location: targetMax.location };
1068
+ }
1069
+ }
1070
+
1071
+ const notNullKeywords = { false: 'null', true: 'not null' };
1072
+
1073
+ function setNullability( art, token1, token2 ) {
1074
+ const notNull = this.valueWithTokenLocation( !!token2, token1, token2 );
1075
+ if (art.notNull)
1076
+ this.reportDuplicateClause( 'notNull', art.notNull, notNull, notNullKeywords );
1077
+ art.notNull = notNull;
1078
+ }
1079
+
1080
+ function reportDuplicateClause( prop, errorneous, chosen, keywords ) {
1081
+ // probably easier for message linters not to use (?:) for the message id...?
1082
+ const args = {
1083
+ '#': prop,
1084
+ code: keywords[chosen.val] || chosen.val,
1085
+ line: chosen.location.line,
1086
+ col: chosen.location.col,
1087
+ };
1088
+ if (errorneous.val === chosen.val)
1089
+ this.warning( 'syntax-duplicate-equal-clause', errorneous.location, args );
1090
+ else
1091
+ this.message( 'syntax-duplicate-clause', errorneous.location, args );
1092
+ }
1093
+
1094
+ const extensionsCode = {
1095
+ definitions: 'extend … with definitions',
1096
+ context: 'extend context',
1097
+ service: 'extend service',
1098
+ };
1099
+
1100
+ function reportUnexpectedExtension( defOnly, token ) {
1101
+ if (defOnly) {
1102
+ this.error( 'syntax-unexpected-extension', token,
1103
+ { keyword: token.text, code: extensionsCode[defOnly] } );
1049
1104
  }
1050
1105
  }
1051
1106