@sap/cds-compiler 3.9.4 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +107 -4
  2. package/README.md +0 -1
  3. package/bin/cdsc.js +11 -23
  4. package/bin/cdsse.js +3 -3
  5. package/doc/API.md +5 -0
  6. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  7. package/doc/CHANGELOG_BETA.md +17 -1
  8. package/doc/CHANGELOG_DEPRECATED.md +28 -0
  9. package/lib/api/.eslintrc.json +1 -1
  10. package/lib/api/main.js +55 -9
  11. package/lib/api/options.js +2 -0
  12. package/lib/base/error.js +2 -0
  13. package/lib/base/message-registry.js +143 -64
  14. package/lib/base/messages.js +213 -107
  15. package/lib/base/model.js +11 -11
  16. package/lib/checks/.eslintrc.json +1 -1
  17. package/lib/checks/annotationsOData.js +2 -2
  18. package/lib/checks/elements.js +1 -1
  19. package/lib/checks/enricher.js +26 -3
  20. package/lib/checks/onConditions.js +67 -12
  21. package/lib/checks/queryNoDbArtifacts.js +106 -105
  22. package/lib/checks/sql-snippets.js +2 -0
  23. package/lib/checks/types.js +12 -6
  24. package/lib/checks/validator.js +2 -2
  25. package/lib/compiler/assert-consistency.js +10 -8
  26. package/lib/compiler/builtins.js +8 -2
  27. package/lib/compiler/checks.js +52 -35
  28. package/lib/compiler/define.js +31 -26
  29. package/lib/compiler/extend.js +120 -65
  30. package/lib/compiler/finalize-parse-cdl.js +12 -43
  31. package/lib/compiler/generate.js +16 -5
  32. package/lib/compiler/index.js +8 -5
  33. package/lib/compiler/kick-start.js +4 -3
  34. package/lib/compiler/populate.js +96 -95
  35. package/lib/compiler/propagator.js +7 -8
  36. package/lib/compiler/resolve.js +377 -103
  37. package/lib/compiler/shared.js +794 -517
  38. package/lib/compiler/tweak-assocs.js +8 -6
  39. package/lib/compiler/utils.js +44 -0
  40. package/lib/edm/annotations/genericTranslation.js +41 -5
  41. package/lib/edm/csn2edm.js +34 -32
  42. package/lib/edm/edm.js +34 -31
  43. package/lib/edm/edmAnnoPreprocessor.js +0 -23
  44. package/lib/edm/edmInboundChecks.js +7 -2
  45. package/lib/edm/edmPreprocessor.js +25 -18
  46. package/lib/edm/edmUtils.js +8 -4
  47. package/lib/gen/Dictionary.json +18 -0
  48. package/lib/gen/language.checksum +1 -1
  49. package/lib/gen/language.interp +4 -2
  50. package/lib/gen/languageParser.js +5006 -4582
  51. package/lib/json/from-csn.js +157 -112
  52. package/lib/json/to-csn.js +60 -89
  53. package/lib/language/antlrParser.js +17 -13
  54. package/lib/language/docCommentParser.js +11 -1
  55. package/lib/language/genericAntlrParser.js +13 -10
  56. package/lib/language/language.g4 +168 -97
  57. package/lib/main.d.ts +128 -36
  58. package/lib/main.js +1 -1
  59. package/lib/model/csnRefs.js +24 -5
  60. package/lib/model/csnUtils.js +9 -8
  61. package/lib/model/revealInternalProperties.js +7 -12
  62. package/lib/model/sortViews.js +4 -2
  63. package/lib/modelCompare/compare.js +1 -1
  64. package/lib/modelCompare/utils/filter.js +40 -2
  65. package/lib/optionProcessor.js +0 -3
  66. package/lib/render/toCdl.js +247 -214
  67. package/lib/render/toHdbcds.js +197 -181
  68. package/lib/render/toSql.js +325 -289
  69. package/lib/render/utils/common.js +42 -4
  70. package/lib/render/utils/delta.js +1 -1
  71. package/lib/render/utils/sql.js +3 -3
  72. package/lib/transform/braceExpression.js +2 -2
  73. package/lib/transform/db/.eslintrc.json +1 -1
  74. package/lib/transform/db/applyTransformations.js +3 -3
  75. package/lib/transform/db/associations.js +24 -12
  76. package/lib/transform/db/expansion.js +17 -18
  77. package/lib/transform/db/flattening.js +17 -21
  78. package/lib/transform/db/rewriteCalculatedElements.js +171 -64
  79. package/lib/transform/db/views.js +3 -4
  80. package/lib/transform/draft/db.js +21 -12
  81. package/lib/transform/draft/odata.js +4 -0
  82. package/lib/transform/forOdataNew.js +62 -47
  83. package/lib/transform/forRelationalDB.js +12 -7
  84. package/lib/transform/localized.js +4 -2
  85. package/lib/transform/odata/toFinalBaseType.js +5 -5
  86. package/lib/transform/odata/typesExposure.js +3 -3
  87. package/lib/transform/parseExpr.js +3 -0
  88. package/lib/transform/transformUtilsNew.js +43 -23
  89. package/lib/transform/translateAssocsToJoins.js +7 -6
  90. package/lib/transform/universalCsn/.eslintrc.json +1 -1
  91. package/lib/transform/universalCsn/coreComputed.js +7 -5
  92. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
  93. package/package.json +2 -2
  94. package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
  95. package/share/messages/message-explanations.json +1 -1
@@ -33,6 +33,7 @@ let universalCsn = false;
33
33
  let strictMode = false; // whether to dump with unknown properties (in standard)
34
34
  let projectionAsQuery = false;
35
35
  let withLocations = false;
36
+ let withDocComments = false;
36
37
  let structXpr = false;
37
38
  let dictionaryPrototype = null;
38
39
 
@@ -54,7 +55,7 @@ const transformers = {
54
55
  // early and modifiers (without null / not null) ---------------------------
55
56
  kind,
56
57
  id: n => n, // in path item
57
- doc: value,
58
+ doc: docComment,
58
59
  '@': anno,
59
60
  virtual: value,
60
61
  key: value,
@@ -159,7 +160,7 @@ const transformers = {
159
160
  // which should appear at that place in order.
160
161
  const csnPropertyNames = {
161
162
  virtual: [ 'abstract' ], // abstract is compiler v1 CSN property
162
- kind: [ 'annotate', 'extend', '$origin' ],
163
+ kind: [ 'annotate', 'extend', '$origin' ], // TODO: $origin better at the end? see addOrigin()
163
164
  op: [ 'join', 'func', 'xpr' ], // TODO: 'func','xpr' into 'quantifier'? TODO: 'global'(scope)?
164
165
  quantifier: [
165
166
  'some', 'any', 'distinct', // 'all' explicitly listed
@@ -521,7 +522,7 @@ function targetAspect( val, csn, node ) {
521
522
  if (universalCsn) {
522
523
  if (val.$inferred)
523
524
  return undefined;
524
- if (node.target) {
525
+ if (node.target) { // TODO: use addOrigin() for this
525
526
  csn.$origin = { target: (val.elements) ? standard( val ) : artifactRef( val, true ) };
526
527
  return undefined;
527
528
  }
@@ -536,13 +537,17 @@ function targetAspect( val, csn, node ) {
536
537
  return undefined;
537
538
  }
538
539
 
539
- function target( val, _csn, node ) {
540
- if (gensrcFlavor && node._origin && node._origin.$inferred === 'REDIRECTED')
541
- val = node._origin.target;
540
+ function target( val, csn, node ) {
542
541
  if (val.elements)
543
542
  return standard( val ); // elements in target (parse-cdl)
543
+ // Mention user-provided target in $origin if outside query entity:
544
+ if (val.$inferred === '' && universalCsn && !gensrcFlavor && !node._main?.query) {
545
+ if (!csn.$origin)
546
+ csn.$origin = {};
547
+ csn.$origin.target = artifactRef( val, '.path' ); // TODO: to addOrigin()
548
+ }
544
549
  if (!universalCsn || gensrcFlavor || node.on)
545
- return artifactRef( val, true );
550
+ return artifactRef( val, !gensrcFlavor || val.$inferred !== '' || '.path' );
546
551
  const tref = artifactRef( val, true );
547
552
  const proto = node.type && !node.type.$inferred ? node.type._artifact : node._origin;
548
553
  return (proto && proto.target && artifactRef( proto.target, true ) === tref)
@@ -668,8 +673,7 @@ function ignore() { /* no-op: ignore property */ }
668
673
 
669
674
  function location( loc, csn, xsn ) {
670
675
  if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' &&
671
- (!xsn.$inferred || !xsn._main) &&
672
- xsn.$inferred !== 'REDIRECTED') { // TODO: also for 'select'
676
+ (!xsn.$inferred || !xsn._main)) { // TODO: also for 'select'
673
677
  // Also include $location for elements in queries (if not via '*')
674
678
  addLocation( xsn.name && xsn.name.location || loc, csn );
675
679
  }
@@ -738,8 +742,6 @@ function foreignKeys( dict, csn, node ) {
738
742
  return;
739
743
 
740
744
  if (gensrcFlavor) {
741
- if (node._origin?.$inferred === 'REDIRECTED')
742
- dict = node._origin.foreignKeys;
743
745
  if (dict[$inferred])
744
746
  return;
745
747
  }
@@ -1050,8 +1052,6 @@ function originRef( art, user ) {
1050
1052
  }
1051
1053
 
1052
1054
  function kind( k, csn, node ) {
1053
- if (node.$inferred === 'REDIRECTED')
1054
- return;
1055
1055
  if (k === 'annotate' || k === 'extend') {
1056
1056
  // We just use `name.absolute` because it is very likely a "constructed"
1057
1057
  // extensions. The CSN parser must produce name.path like for other refs.
@@ -1077,18 +1077,11 @@ function kind( k, csn, node ) {
1077
1077
  csn.$generated = generated;
1078
1078
  }
1079
1079
 
1080
- function type( node, csn, xsn ) {
1080
+ function type( node ) {
1081
1081
  if (!universalCsn)
1082
1082
  return artifactRef( node, !node.$extra );
1083
1083
  if (node.$inferred && node.$inferred !== 'cast')
1084
1084
  return undefined;
1085
- if (xsn._origin) {
1086
- if (xsn._origin.$inferred === 'REDIRECTED') { // auto-redirected user-provided target
1087
- const $origin = definition( xsn._origin );
1088
- if ($origin) // if not rendered as column
1089
- csn.$origin = $origin;
1090
- }
1091
- }
1092
1085
  return artifactRef( node, !node.$extra );
1093
1086
  }
1094
1087
 
@@ -1106,73 +1099,39 @@ function artifactRef( node, terse ) {
1106
1099
  if (node.$inferred && gensrcFlavor)
1107
1100
  return undefined;
1108
1101
  // Works also on XSN directly coming from parser and with XSN from CDL->CSN transformation
1109
- const { path } = node;
1110
- if (terse && node._artifact && !node._artifact._main)
1102
+ // Shortcut for many cases:
1103
+ if (terse && node._artifact && !node._artifact._main && terse !== '.path')
1111
1104
  return node._artifact.name.absolute;
1105
+ let { path } = node;
1112
1106
  if (!path)
1113
1107
  return undefined; // TODO: complain with strict
1114
- else if (!path.length)
1108
+ if (!path.length)
1115
1109
  return [];
1116
1110
 
1117
- const link = path[0]._artifact; // XSN TODO: store double definitions differently
1118
- const root = Array.isArray(link) ? link[0] : link;
1119
- if (!root) { // XSN directly coming from the parser
1120
- if (strictMode && node.scope === 'typeOf')
1121
- throw new CompilerAssertion( `Unexpected TYPE OF in ${ locationString(node.location) }`);
1122
- return renderArtifactPath( node, path, terse, node.scope );
1123
- }
1124
- const { absolute } = root.name;
1125
- if (node.scope !== 'typeOf' && typeof node.scope !== 'number') {
1126
- // CSN input or generated in compiler (XSN TODO: remove scope:'global')
1127
- if (absolute === path[0].id) // normal case (no localization view)
1128
- return renderArtifactPath( node, path, terse );
1129
- // scope:param is not valid (and would be lost)
1130
- const head = Object.assign( {}, path[0], { id: absolute } );
1131
- return renderArtifactPath( node, [ head, ...path.slice(1) ], terse );
1132
- }
1133
- if (node.scope === 'typeOf') { // TYPE OF without ':' in path
1134
- // Root _artifact which is either element or main artifact for paths starting with $self.
1135
- // To make the CDL->CSN transformation simpler, the _artifact for first item could be
1136
- // a fake element with just a correct absolute name and _parent/_main links.
1137
- if (!root._main || root.kind === 'select') { // $self/$projection
1138
- // in query, only correct for leading query ->
1139
- // TODO: forbid TYPE OF elem / TYPE OF $self.elem in queries
1140
- return renderArtifactPath( node, [ { id: absolute }, ...path.slice(1) ], terse );
1141
- }
1142
- const parent = root._parent;
1143
- const structs = parent.name.element ? parent.name.element.split('.') : [];
1144
- return extra( { ref: [ absolute, ...structs, ...path.map( pathItem ) ] }, node );
1145
- }
1146
- let { scope } = node;
1147
- if (!scope) { // no ':' in CDL path - try to be nice and guess it via links
1148
- const { length } = path;
1149
- for (; scope < length; ++scope) {
1150
- const art = path[scope]._artifact;
1151
- if (!art) {
1152
- scope = 0; // unsuccessful, not all path items have links
1153
- break;
1154
- }
1155
- if (art._main)
1156
- break; // successful, found first element
1157
- }
1158
- }
1159
- const head = Object.assign( {}, path[0], { id: absolute } );
1160
- return renderArtifactPath( node, [ head, ...path.slice(1) ], terse, scope );
1161
- }
1111
+ const head = path[0];
1112
+ const root = head._artifact;
1113
+ const id = root?.name.absolute;
1114
+ const scope = node.scope || path.length;
1162
1115
 
1163
- function renderArtifactPath( node, path, terse, scope ) {
1164
- if (scope === 0) {
1165
- // try to find ':' position syntactically for FROM
1166
- scope = !terse && path.findIndex( i => i.where || i.args || i.cardinality) + 1 ||
1167
- path.length;
1168
- }
1169
1116
  if (typeof scope === 'number' && scope > 1) {
1170
1117
  const item = path[scope - 1];
1171
- const name = item._artifact && item._artifact.name;
1172
- // In localization views, the _artifact link of `item` is important
1173
- const id = name && name.absolute ||
1174
- pathName( path.slice( 0, scope ) );
1175
- path = [ Object.assign( {}, item, { id } ), ...path.slice( scope ) ];
1118
+ const name = item._artifact?.name;
1119
+ const absolute = name?.absolute ||
1120
+ `${ id || head.id }.${ path.slice( 1, scope ).map( i => i.id ).join('.') }`;
1121
+ path = [ Object.assign( {}, item, { id: absolute } ), ...path.slice( scope ) ];
1122
+ }
1123
+ else if (scope === 'typeOf') { // TYPE OF without ':' in path
1124
+ if (root) {
1125
+ const structs = root.name.element?.split('.').map( n => ({ id: n }) );
1126
+ // TODO: change (follow parents) if we introduce sparse names
1127
+ path = [ { id }, ...(structs || []), ...path.slice(1) ];
1128
+ }
1129
+ else if (strictMode) {
1130
+ throw new CompilerAssertion( `Unexpected TYPE OF in ${ locationString(node.location) }`);
1131
+ }
1132
+ }
1133
+ else if (root && id !== head.id) {
1134
+ path = [ Object.assign( {}, head, { id } ), ...path.slice( 1 ) ];
1176
1135
  }
1177
1136
  const ref = path.map( pathItem );
1178
1137
  return (!terse || ref.length !== 1 || typeof ref[0] !== 'string')
@@ -1183,6 +1142,10 @@ function renderArtifactPath( node, path, terse, scope ) {
1183
1142
  function pathItem( item ) {
1184
1143
  if (!item.args &&
1185
1144
  !item.where &&
1145
+ !item.groupBy &&
1146
+ !item.having &&
1147
+ !item.limit &&
1148
+ !item.orderBy &&
1186
1149
  !item.cardinality &&
1187
1150
  !item.$extra &&
1188
1151
  !item.$syntax)
@@ -1205,8 +1168,15 @@ function anno( node ) {
1205
1168
  return value(node);
1206
1169
  }
1207
1170
 
1171
+ function docComment( doc ) {
1172
+ // Value is `true` if options.docComment is falsey for CDL input.
1173
+ if (withDocComments && doc?.val !== true)
1174
+ return value( doc );
1175
+ return undefined;
1176
+ }
1177
+
1208
1178
  function value( node ) {
1209
- // "Short" value form, e.g. for annotation assignments
1179
+ // "Short" value form, e.g. for annotation assignments
1210
1180
  if (!node)
1211
1181
  return true; // `@aBool` short for `@aBool: true`
1212
1182
  if (universalCsn && node.$inferred) {
@@ -1242,22 +1212,21 @@ function value( node ) {
1242
1212
  function enumValueOrCalc( v, csn, node ) {
1243
1213
  if (v.$inferred && (universalCsn || gensrcFlavor))
1244
1214
  return undefined;
1245
- // Enums can have values but if enums are extended, their kind is 'element'
1246
- if (node.kind === 'enum' || node.$syntax === 'enum') {
1215
+ // Enums can have values but if enums are extended, their kind is 'element'.
1216
+ // In v4, we don't check `node.$syntax === 'enum'` anymore.
1217
+ if (node.kind === 'enum') {
1247
1218
  Object.assign( csn, expression( v ) );
1248
1219
  }
1249
- else if (node.$syntax === 'calc') { // TODO: || node._parent?.kind === 'extend'
1220
+ else if (node.$syntax === 'calc' || node._parent?.kind === 'extend') {
1250
1221
  const stored = v.stored ? { stored: value(v.stored) } : {};
1251
1222
  return Object.assign( stored, expression( v ) );
1252
1223
  }
1253
1224
  return undefined;
1254
1225
  }
1255
1226
 
1256
- function onCondition( cond, csn, node ) {
1227
+ function onCondition( cond ) {
1257
1228
  if (gensrcFlavor) {
1258
- if (node._origin && node._origin.$inferred === 'REDIRECTED')
1259
- cond = node._origin.on;
1260
- else if (cond.$inferred)
1229
+ if (cond.$inferred)
1261
1230
  return undefined;
1262
1231
  }
1263
1232
  return condition( cond );
@@ -1475,6 +1444,7 @@ function addElementAsColumn( elem, cols ) {
1475
1444
  try {
1476
1445
  gensrcFlavor = gensrcFlavor || 'column';
1477
1446
  set( 'virtual', col, elem );
1447
+ // TODO if (!elem.key?.$specifiedElement)
1478
1448
  set( 'key', col, elem );
1479
1449
  const expr = expression( elem.value );
1480
1450
  Object.assign( col, (expr.cast ? { xpr: [ expr ] } : expr) );
@@ -1533,7 +1503,7 @@ function cast( csn, node ) {
1533
1503
  else
1534
1504
  r.cast = {}; // TODO: what about $extra in cast?
1535
1505
  for (const prop of typeProperties)
1536
- set( prop, r.cast, node );
1506
+ set(prop, r.cast, node);
1537
1507
  return r;
1538
1508
  }
1539
1509
 
@@ -1596,6 +1566,7 @@ function initModuleVars( options = { csnFlavor: 'gensrc' } ) {
1596
1566
  ? proto
1597
1567
  : (proto) ? Object.prototype : null;
1598
1568
  withLocations = options.withLocations;
1569
+ withDocComments = options.docComment !== false;
1599
1570
  structXpr = options.structXpr;
1600
1571
  projectionAsQuery = isDeprecatedEnabled( options, '_projectionAsQuery' );
1601
1572
  }
@@ -11,7 +11,6 @@
11
11
  const antlr4 = require('antlr4');
12
12
 
13
13
  const { CompileMessage } = require('../base/messages');
14
- const { isBetaEnabled } = require('../base/model');
15
14
  const errorStrategy = require('./errorStrategy');
16
15
 
17
16
  const Parser = require('../gen/languageParser').default;
@@ -30,11 +29,6 @@ class ErrorListener extends antlr4.error.ErrorListener {
30
29
  }
31
30
 
32
31
  class RewriteTypeTokenStream extends antlr4.CommonTokenStream {
33
- constructor(lexer, v4newKeyword) {
34
- super(lexer);
35
- this.v4newKeyword = v4newKeyword;
36
- }
37
-
38
32
  LT( k ) {
39
33
  const t = super.LT(k);
40
34
  if (!t || !t.type)
@@ -56,7 +50,7 @@ class RewriteTypeTokenStream extends antlr4.CommonTokenStream {
56
50
  else if (t.type === this.NEW) {
57
51
  const n = super.LT(k + 1);
58
52
  // TODO v4: rewrite token in grammar via `this.setLocalToken`
59
- if (n?.type === this.Identifier && (this.v4newKeyword || /^st_/i.test( n.text ))) {
53
+ if (n?.type === this.Identifier) {
60
54
  const o = super.LT(k + 2);
61
55
  if (o?.type === this.PAREN)
62
56
  return t;
@@ -118,8 +112,7 @@ function parse( source, filename = '<undefined>.cds',
118
112
  options = {}, messageFunctions = null,
119
113
  rule = 'cdl' ) {
120
114
  const lexer = new Lexer( new antlr4.InputStream(source) );
121
- const v4newKeyword = isBetaEnabled(options, 'v4preview');
122
- const tokenStream = new RewriteTypeTokenStream(lexer, v4newKeyword);
115
+ const tokenStream = new RewriteTypeTokenStream(lexer);
123
116
  /** @type {object} */
124
117
  const parser = new Parser( tokenStream );
125
118
  const errorListener = new ErrorListener();
@@ -161,19 +154,30 @@ function parse( source, filename = '<undefined>.cds',
161
154
 
162
155
 
163
156
  const rulespec = rules[rule];
164
- const tree = rule && parser[rulespec.func]();
157
+ let tree;
158
+ try {
159
+ tree = rule && parser[rulespec.func]();
160
+ }
161
+ catch (e) {
162
+ if (e instanceof RangeError && e.message.match(/Maximum.*exceeded$/i)) {
163
+ messageFunctions.error('syntax-invalid-source', { file: filename },
164
+ { '#': 'cdl-stackoverflow' } );
165
+ }
166
+ else {
167
+ throw e;
168
+ }
169
+ }
165
170
  const ast = tree && tree[rulespec.returns] || {};
166
171
  ast.options = options;
167
172
  if (rulespec.$frontend)
168
173
  ast.$frontend = rulespec.$frontend;
169
174
 
170
- // Warn about unused doc-comments.
171
175
  // Do not warn if docComments are explicitly disabled.
172
176
  if (options.docComment !== false) {
173
177
  for (const token of tokenStream.tokens) {
174
178
  if (token.type === parser.constructor.DocComment && !token.isUsed) {
175
- messageFunctions.info( 'syntax-ignoring-doc-comment', parser.tokenLocation(token), {},
176
- 'Ignoring doc comment as it is not written at a defined position' );
179
+ messageFunctions.info('syntax-ignoring-doc-comment', parser.tokenLocation(token), {},
180
+ 'Ignoring doc comment as it is not written at a defined position');
177
181
  }
178
182
  }
179
183
  }
@@ -15,6 +15,12 @@ const hasContentOnFirstLineRegEx = /\/\*+\s*\S/;
15
15
  * If the comment only contains whitespace it is seen as empty and `null` is returned
16
16
  * which also stops doc comment propagation.
17
17
  *
18
+ * Notes on escape sequences:
19
+ * - For `*\/`, the `\` is removed.
20
+ * - Nothing else is escaped, meaning `\n` will be `\` and `n` and not a newline character.
21
+ * - _If requested_, we could parse the doc comment similar to multiline string literals, but
22
+ * via an option.
23
+ *
18
24
  * @param {string} comment Raw comment, e.g. '/** comment ... '.
19
25
  * Must be a valid doc comment.
20
26
  * @returns {string|null} Parsed contents or if the comment has an invalid format or
@@ -34,6 +40,7 @@ function parseDocComment( comment ) {
34
40
  const content = lines[0]
35
41
  .replace(/^\/[*]{2,}/, '')
36
42
  .replace(/\*+\/$/, '')
43
+ .replace('*\\/', '*/') // escape sequence
37
44
  .trim();
38
45
  return isWhitespaceOrNewLineOnly(content) ? null : content;
39
46
  }
@@ -66,7 +73,10 @@ function parseDocComment( comment ) {
66
73
  const startIndex = (lines[0] === '') ? 1 : 0;
67
74
  const endIndex = (lines[lines.length - 1] === '') ? lines.length - 1 : lines.length;
68
75
 
69
- const content = lines.slice(startIndex, endIndex).join('\n');
76
+ const content = lines
77
+ .slice(startIndex, endIndex)
78
+ .join('\n')
79
+ .replace('*\\/', '*/'); // escape sequence
70
80
  return isWhitespaceOrNewLineOnly(content) ? null : content;
71
81
  }
72
82
 
@@ -401,7 +401,8 @@ function checkExtensionDict( dict ) {
401
401
  this.addAnnotation( def, prop, dup[prop] );
402
402
  }
403
403
  else if (prop === 'doc') {
404
- if (def.doc) {
404
+ // With explicit docComment:false, we don't emit a warning.
405
+ if (def.doc && this.options.docComment !== false) {
405
406
  this.warning( 'syntax-duplicate-doc-comment', def.doc.location, {},
406
407
  'Doc comment is overwritten by another one below' );
407
408
  }
@@ -646,13 +647,15 @@ function docComment( node ) {
646
647
  // This token is actually used by / assigned to an artifact.
647
648
  token.isUsed = true;
648
649
 
649
- if (!this.options.docComment)
650
- return;
651
- if (node.doc) {
650
+ // With explicit docComment:false, we don't emit a warning.
651
+ if (node.doc && this.options.docComment !== false) {
652
652
  this.warning( 'syntax-duplicate-doc-comment', node.doc.location, {},
653
653
  'Doc comment is overwritten by another one below' );
654
654
  }
655
- node.doc = this.valueWithTokenLocation( parseDocComment( token.text ), token );
655
+
656
+ // Either store the doc comment or a marker that there is one.
657
+ const val = !this.options.docComment ? true : parseDocComment( token.text );
658
+ node.doc = this.valueWithTokenLocation( val, token );
656
659
  }
657
660
 
658
661
  // Classify token (identifier category) for implicit names,
@@ -1099,7 +1102,7 @@ function aspectWithoutElements( art ) {
1099
1102
  // TODO: Checking it here does not prevent aspect in CSN input having no elements!
1100
1103
  art.elements = this.createDict();
1101
1104
  if (!isBetaEnabled( this.options, 'aspectWithoutElements' )) {
1102
- this.error( null, [ art.name.location ], {},
1105
+ this.error( null, [ art.name.location, null ], {},
1103
1106
  'Aspects without elements are not supported, yet' );
1104
1107
  }
1105
1108
  }
@@ -1189,7 +1192,7 @@ function setNullability( art, token1, token2 ) {
1189
1192
  art.notNull = notNull;
1190
1193
  }
1191
1194
 
1192
- function reportDuplicateClause( prop, errorneous, chosen, keywords ) {
1195
+ function reportDuplicateClause( prop, erroneous, chosen, keywords ) {
1193
1196
  // probably easier for message linters not to use (?:) for the message id...?
1194
1197
  const args = {
1195
1198
  '#': prop,
@@ -1197,10 +1200,10 @@ function reportDuplicateClause( prop, errorneous, chosen, keywords ) {
1197
1200
  line: chosen.location.line,
1198
1201
  col: chosen.location.col,
1199
1202
  };
1200
- if (errorneous.val === chosen.val)
1201
- this.warning( 'syntax-duplicate-equal-clause', errorneous.location, args );
1203
+ if (erroneous.val === chosen.val)
1204
+ this.warning( 'syntax-duplicate-equal-clause', erroneous.location, args );
1202
1205
  else
1203
- this.message( 'syntax-duplicate-clause', errorneous.location, args );
1206
+ this.message( 'syntax-duplicate-clause', erroneous.location, args );
1204
1207
  }
1205
1208
 
1206
1209
  const extensionsCode = {