@sap/cds-compiler 2.11.4 → 2.12.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 (80) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/bin/cds_update_identifiers.js +7 -7
  3. package/bin/cdsc.js +9 -10
  4. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  5. package/doc/CHANGELOG_BETA.md +12 -0
  6. package/lib/api/main.js +2 -0
  7. package/lib/api/options.js +2 -2
  8. package/lib/base/message-registry.js +31 -2
  9. package/lib/base/model.js +1 -0
  10. package/lib/base/optionProcessorHelper.js +97 -69
  11. package/lib/checks/.eslintrc.json +2 -0
  12. package/lib/checks/actionsFunctions.js +2 -1
  13. package/lib/checks/foreignKeys.js +4 -4
  14. package/lib/checks/managedInType.js +4 -4
  15. package/lib/checks/queryNoDbArtifacts.js +1 -3
  16. package/lib/checks/sql-snippets.js +93 -0
  17. package/lib/checks/validator.js +8 -0
  18. package/lib/compiler/assert-consistency.js +5 -3
  19. package/lib/compiler/base.js +0 -1
  20. package/lib/compiler/checks.js +32 -9
  21. package/lib/compiler/definer.js +25 -4
  22. package/lib/compiler/index.js +1 -1
  23. package/lib/compiler/propagator.js +3 -2
  24. package/lib/compiler/resolver.js +97 -6
  25. package/lib/compiler/shared.js +12 -1
  26. package/lib/compiler/utils.js +7 -0
  27. package/lib/edm/annotations/genericTranslation.js +34 -17
  28. package/lib/edm/annotations/preprocessAnnotations.js +1 -1
  29. package/lib/edm/csn2edm.js +1 -1
  30. package/lib/edm/edm.js +8 -8
  31. package/lib/edm/edmPreprocessor.js +30 -23
  32. package/lib/edm/edmUtils.js +11 -12
  33. package/lib/gen/Dictionary.json +82 -40
  34. package/lib/gen/language.checksum +1 -1
  35. package/lib/gen/language.interp +3 -1
  36. package/lib/gen/language.tokens +15 -14
  37. package/lib/gen/languageLexer.interp +9 -1
  38. package/lib/gen/languageLexer.js +830 -779
  39. package/lib/gen/languageLexer.tokens +7 -6
  40. package/lib/gen/languageParser.js +2401 -2282
  41. package/lib/json/from-csn.js +47 -16
  42. package/lib/json/to-csn.js +17 -5
  43. package/lib/language/antlrParser.js +3 -3
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/genericAntlrParser.js +68 -51
  46. package/lib/language/language.g4 +128 -74
  47. package/lib/language/multiLineStringParser.js +536 -0
  48. package/lib/main.d.ts +5 -3
  49. package/lib/main.js +3 -2
  50. package/lib/model/csnRefs.js +116 -68
  51. package/lib/model/csnUtils.js +40 -48
  52. package/lib/model/enrichCsn.js +30 -14
  53. package/lib/optionProcessor.js +3 -3
  54. package/lib/render/DuplicateChecker.js +1 -1
  55. package/lib/render/manageConstraints.js +1 -1
  56. package/lib/render/toCdl.js +193 -79
  57. package/lib/render/toHdbcds.js +179 -95
  58. package/lib/render/toRename.js +7 -10
  59. package/lib/render/toSql.js +57 -40
  60. package/lib/render/utils/common.js +24 -5
  61. package/lib/render/utils/sql.js +6 -4
  62. package/lib/transform/braceExpression.js +4 -2
  63. package/lib/transform/db/associations.js +389 -0
  64. package/lib/transform/db/cdsPersistence.js +150 -0
  65. package/lib/transform/db/constraints.js +6 -4
  66. package/lib/transform/db/draft.js +3 -2
  67. package/lib/transform/db/expansion.js +4 -5
  68. package/lib/transform/db/flattening.js +5 -6
  69. package/lib/transform/db/temporal.js +236 -0
  70. package/lib/transform/db/transformExists.js +36 -23
  71. package/lib/transform/forHanaNew.js +35 -626
  72. package/lib/transform/forOdataNew.js +5 -4
  73. package/lib/transform/localized.js +3 -14
  74. package/lib/transform/odata/generateForeignKeyElements.js +2 -2
  75. package/lib/transform/transformUtilsNew.js +13 -13
  76. package/lib/transform/translateAssocsToJoins.js +8 -8
  77. package/lib/transform/universalCsnEnricher.js +217 -47
  78. package/lib/utils/file.js +2 -1
  79. package/lib/utils/timetrace.js +8 -2
  80. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,12 +7,69 @@
7
7
  Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
8
8
  The compiler behavior concerning `beta` features can change at any time without notice.
9
9
 
10
+ ## Version 2.12.0 - 2022-01-25
11
+
12
+ ### Added
13
+
14
+ - CDL parser: You can now use multiline string literals and text blocks.
15
+ Use backticks (\`) for string literals that can span multiple lines and can use JavaScript-like escape
16
+ sequences such as `\u{0020}`. You can also use three backticks (\`\`\`) for strings (a.k.a. text blocks)
17
+ which are automatically indentation-stripped and can have an optional language identifier that is used
18
+ for syntax highlighting, similar to markdown. In difference to the former, text blocks require the
19
+ opening and closing backticks to be on separate lines.
20
+ Example:
21
+
22
+ @annotation: `Multi
23
+ line\u{0020}strings`
24
+
25
+ @textblock: ```xml
26
+ <summary>
27
+ <detail>The root tag has no indentation in this example</detail>
28
+ </summary>
29
+ ```
30
+ ...
31
+
32
+ - Enhance the ellipsis operator `...` for array annotations by an `up to ‹val›`:
33
+ only values in the array of the base annotation up to (including) the first match
34
+ of the specified `‹val›` are included at the specified place in the final array value.
35
+ An array annotation can have more than on `... up to ‹val›` items and must also
36
+ have a pure `...` item after them.
37
+ A structured `‹val›` matches if the array item is also a structure and all property
38
+ values in `‹val›` are equal to the corresponding property value in the array value;
39
+ it is not necessary to specify all properties of the array value items in `‹val›`.
40
+ Example
41
+
42
+ @Anno: [{name: one, val: 1}, {name: two, val: 2}, {name: four, val: 4}]
43
+ type T: Integer;
44
+ @Anno: [{name: zero, val: 0}, ... up to {name: two}, {name: three, val: 3}, ...]
45
+ annotate T;
46
+
47
+ - for.odata: Support `@cds.on {update|insert}` as replacement for deprecated `@odata.on { update|insert }` to
48
+ set `@Core.Computed`.
49
+
50
+ ### Changed
51
+
52
+ - Update OData Vocabularies 'Aggregation', 'Capabilities', 'Common', 'Core', PersonalData, 'Session', 'UI'
53
+
54
+ ### Fixed
55
+
56
+ - to.sql/hdi/hdbcds: With `exists`, ensure that the precedence of the existing association-on-conditions and where-conditions is kept by adding braces.
57
+ - to.sql/hdi: Window function suffixes are now properly rendered.
58
+ - to.sql: `$self` comparisons inside aspects are not checked and won't result in an error anymore.
59
+ - to.hdbcds:
60
+ + Correctly apply the "."-to-"_"-translation algorithm to artifacts that are marked with `@cds.persistence.exists`.
61
+ + Message with ID `anno-hidden-exists` (former `anno-unstable-hdbcds`) is now
62
+ only issued if the compiler generates a SAP HANA CDS artifact which would hide
63
+ a native database object from being resolved in a SAP HANA CDS `using … as …`.
64
+ - to.cdl: Annotation paths containing special characters such as spaces or `@` are now quoted, e.g. `@![some@annotation]`.
65
+ - compiler: A warning is emitted for elements of views with localized keys as the localized property is ignored for them.
66
+
10
67
  ## Version 2.11.4 - 2021-12-21
11
68
 
12
69
  ### Fixed
13
70
 
14
71
  - CDL parser: in many situations, improve message when people use reserved keywords as identifier
15
- - Improve error text and error location for ambiguious auto-redirection target
72
+ - Improve error text and error location for ambiguous auto-redirection target
16
73
  - to.sql/hdi/hdbcds:
17
74
  + Correctly detect `exists` in projections
18
75
  + Correctly handle elements starting with `$` in the on-condition of associations
@@ -30,22 +30,22 @@ const fs = require('fs');
30
30
  const path = require('path');
31
31
 
32
32
  const cliArgs = process.argv.slice(2);
33
- const filename = cliArgs[0];
33
+ const filepath = cliArgs[0];
34
34
 
35
35
  if (cliArgs.length !== 1)
36
36
  exitError(`Expected exactly one argument, ${cliArgs.length} given`);
37
37
 
38
- if (!filename)
39
- exitError('Expected non-empty filename as argument!');
38
+ if (!filepath)
39
+ exitError('Expected non-empty filepath as argument!');
40
40
 
41
41
  // Do not use allow-list approach.
42
42
  // There may be CDS files with other extensions than `.cds`.
43
- if (filename.endsWith('.csn') || filename.endsWith('.json'))
43
+ if (filepath.endsWith('.csn') || filepath.endsWith('.json'))
44
44
  exitError('Only CDS files can be passed! Found CSN file!');
45
45
 
46
- let source = fs.readFileSync(filename, 'utf-8');
47
- source = modernizeIdentifierStyle(source, filename);
48
- fs.writeFileSync(filename, source);
46
+ let sourceStr = fs.readFileSync(filepath, 'utf-8');
47
+ sourceStr = modernizeIdentifierStyle(sourceStr, filepath);
48
+ fs.writeFileSync(filepath, sourceStr);
49
49
  process.exit(0); // success
50
50
 
51
51
  // --------------------------------------------------------
package/bin/cdsc.js CHANGED
@@ -227,7 +227,7 @@ function displayUsage(error, helpText, code) {
227
227
  out.write(`${helpText}\n`);
228
228
  if (error) {
229
229
  if (error instanceof Array)
230
- out.write(`${error.map(error => `cdsc: ERROR: ${error}`).join('\n')}\n`);
230
+ out.write(`${error.map(err => `cdsc: ERROR: ${err}`).join('\n')}\n`);
231
231
  else
232
232
  out.write(`cdsc: ERROR: ${error}\n`);
233
233
  }
@@ -301,11 +301,11 @@ function executeCommandLine(command, options, args) {
301
301
  // Return the original model (for chaining)
302
302
  function toCsn( model ) {
303
303
  if (options.directBackend) {
304
- displayNamedCsn(model, 'csn', options);
304
+ displayNamedCsn(model, 'csn');
305
305
  }
306
306
  else {
307
307
  // Result already provided by caller
308
- displayNamedXsn(model, 'csn', options);
308
+ displayNamedXsn(model, 'csn');
309
309
  }
310
310
  return model;
311
311
  }
@@ -316,7 +316,7 @@ function executeCommandLine(command, options, args) {
316
316
  const csn = options.directBackend ? model : compactModel(model, options);
317
317
 
318
318
  if (options.toHana && options.toHana.csn) {
319
- displayNamedCsn(for_hdbcds(csn, remapCmdOptions(options, options.toHana)), 'hana_csn', options);
319
+ displayNamedCsn(for_hdbcds(csn, remapCmdOptions(options, options.toHana)), 'hana_csn');
320
320
  }
321
321
  else {
322
322
  const hanaResult = main.to.hdbcds(csn, remapCmdOptions(options, options.toHana));
@@ -339,7 +339,7 @@ function executeCommandLine(command, options, args) {
339
339
  const csn = options.directBackend ? model : compactModel(model, options);
340
340
  const odataCsn = main.for.odata(csn, remapCmdOptions(options, options.toOdata));
341
341
  if (options.toOdata && options.toOdata.csn) {
342
- displayNamedCsn(odataCsn, 'odata_csn', options);
342
+ displayNamedCsn(odataCsn, 'odata_csn');
343
343
  }
344
344
  else if (options.toOdata && options.toOdata.json) {
345
345
  const result = main.to.edm.all(odataCsn, options);
@@ -392,7 +392,7 @@ function executeCommandLine(command, options, args) {
392
392
  const csn = options.directBackend ? model : compactModel(model, options);
393
393
  if (options.toSql && options.toSql.src === 'hdi') {
394
394
  if (options.toSql.csn) {
395
- displayNamedCsn(for_hdi(csn, remapCmdOptions(options, options.toSql)), 'hdi_csn', options);
395
+ displayNamedCsn(for_hdi(csn, remapCmdOptions(options, options.toSql)), 'hdi_csn');
396
396
  }
397
397
  else {
398
398
  const hdiResult = main.to.hdi(csn, remapCmdOptions(options, options.toSql));
@@ -401,7 +401,7 @@ function executeCommandLine(command, options, args) {
401
401
  }
402
402
  }
403
403
  else if (options.toSql && options.toSql.csn) {
404
- displayNamedCsn(for_sql(csn, remapCmdOptions(options, options.toSql)), 'sql_csn', options);
404
+ displayNamedCsn(for_sql(csn, remapCmdOptions(options, options.toSql)), 'sql_csn');
405
405
  }
406
406
  else {
407
407
  const sqlResult = main.to.sql(csn, remapCmdOptions(options, options.toSql));
@@ -505,7 +505,7 @@ function executeCommandLine(command, options, args) {
505
505
  // or display it to stdout if 'options.out' is '-'.
506
506
  // Depending on 'options.rawOutput', the model is either compacted to 'name.json' or
507
507
  // written in raw form to '<name>_raw.txt'.
508
- function displayNamedXsn(xsn, name, options) {
508
+ function displayNamedXsn(xsn, name) {
509
509
  if (options.rawOutput) {
510
510
  writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn, options.rawOutput), false, null), true);
511
511
  }
@@ -525,9 +525,8 @@ function executeCommandLine(command, options, args) {
525
525
  /**
526
526
  * @param {CSN.Model} csn
527
527
  * @param {string} name
528
- * @param {CSN.Options} options
529
528
  */
530
- function displayNamedCsn(csn, name, options) {
529
+ function displayNamedCsn(csn, name) {
531
530
  if (!csn) // only print CSN if it is set.
532
531
  return;
533
532
  if (options.internalMsg) {
@@ -1516,7 +1516,7 @@ Changes
1516
1516
  * Preserve the `key` properties of elements selected in a view (like we do in projections).
1517
1517
  * Improve the CSN representation for views.
1518
1518
  Represent the `where` and `on` condition of `select`s like other conditions.
1519
- * Project name in github is now `cdx/cds-compiler`.
1519
+ * Project name in github is now `cap/cds-compiler`.
1520
1520
 
1521
1521
  Features
1522
1522
  * Support `select *` in views.
@@ -8,6 +8,12 @@ Note: `beta` fixes, changes and features are listed in this ChangeLog just for i
8
8
  The compiler behavior concerning `beta` features can change at any time without notice.
9
9
  **Don't use `beta` fixes, changes and features in productive mode.**
10
10
 
11
+ ## Version 2.12.0 - 2022-01-25
12
+
13
+ ### Added `sqlSnippets`
14
+
15
+ - to.sql/hdi/hdbcds: Introduce the annotations `@sql.prepend` and `@sql.append` that allow inserting user-written SQL snippets into the compiler generated content.
16
+
11
17
  ## Version 2.11.0
12
18
 
13
19
  ### Removed `foreignKeyConstraints`
@@ -163,6 +169,12 @@ The association to join transformation treats foreign key accesses with priority
163
169
 
164
170
  Unique constraints are now generally available.
165
171
 
172
+ ## Version 1.33.0 - 2020-08-24
173
+
174
+ ### Added `hanaAssocRealCardinality`
175
+
176
+ Render JOIN cardinality in native HANA association if provided. If no cardinality has been specified.
177
+
166
178
  ## Version 1.32.0 - 2020-07-10
167
179
 
168
180
  ### Removed `aspectCompositions`
package/lib/api/main.js CHANGED
@@ -165,6 +165,7 @@ function cdl(csn, externalOptions = {}) {
165
165
  */
166
166
  function forSql(csn, options = {}) {
167
167
  const internalOptions = prepareOptions.to.sql(options);
168
+ internalOptions.transformation = 'sql';
168
169
  internalOptions.toSql.csn = true;
169
170
  return backends.toSqlWithCsn(csn, internalOptions).csn;
170
171
  }
@@ -207,6 +208,7 @@ function forHdbcds(csn, options = {}) {
207
208
  */
208
209
  function sql(csn, options = {}) {
209
210
  const internalOptions = prepareOptions.to.sql(options);
211
+ internalOptions.transformation = 'sql';
210
212
 
211
213
  // we need the CSN for view sorting
212
214
  internalOptions.toSql.csn = true;
@@ -84,9 +84,9 @@ function translateOptions(input = {}, defaults = {}, hardRequire = {},
84
84
  for (const name of overallOptions) {
85
85
  // Ensure that arrays are not passed as a reference!
86
86
  // This caused issues with the way messages are handled in processMessages
87
- if (Array.isArray(input[name]) && inputOptionNames.indexOf(name) !== -1)
87
+ if (Array.isArray(input[name]) && inputOptionNames.includes(name))
88
88
  options[name] = [ ...input[name] ];
89
- else if (inputOptionNames.indexOf(name) !== -1)
89
+ else if (inputOptionNames.includes(name))
90
90
  options[name] = input[name];
91
91
  }
92
92
 
@@ -42,12 +42,15 @@ const centralMessages = {
42
42
  'anno-definition': { severity: 'Warning' },
43
43
  'anno-duplicate': { severity: 'Error', configurableFor: true }, // does not hurt us
44
44
  'anno-duplicate-unrelated-layer': { severity: 'Error', configurableFor: true }, // does not hurt us
45
+ 'anno-invalid-sql-element': { severity: 'Error'}, // @sql.prepend/append
46
+ 'anno-invalid-sql-struct': { severity: 'Error'}, // @sql.prepend/append
47
+ 'anno-invalid-sql-view': { severity: 'Error' }, // @sql.prepend/append
48
+ 'anno-invalid-sql-view-element': { severity: 'Error'}, // @sql.prepend/append
45
49
  'anno-undefined-action': { severity: 'Info' },
46
50
  'anno-undefined-art': { severity: 'Info' }, // for annotate statement (for CDL path root)
47
51
  'anno-undefined-def': { severity: 'Info' }, // for annotate statement (for CSN or CDL path cont)
48
52
  'anno-undefined-element': { severity: 'Info' },
49
53
  'anno-undefined-param': { severity: 'Info' },
50
- 'anno-unstable-hdbcds': { severity: 'Warning' },
51
54
 
52
55
  'args-expected-named': { severity: 'Error', configurableFor: 'deprecated' }, // future --sloppy
53
56
  'args-no-params': { severity: 'Error', configurableFor: 'deprecated' }, // future --sloppy
@@ -128,6 +131,11 @@ const centralMessages = {
128
131
  'syntax-fragile-alias': { severity: 'Error', configurableFor: true },
129
132
  'syntax-fragile-ident': { severity: 'Error', configurableFor: true },
130
133
 
134
+ 'syntax-invalid-text-block' : { severity: 'Error' },
135
+ 'syntax-unknown-escape': { severity: 'Error', configurableFor: true },
136
+ 'syntax-invalid-escape': { severity: 'Error' },
137
+ 'syntax-missing-escape': { severity: 'Error' },
138
+
131
139
  'type-managed-composition': { severity: 'Error', configurableFor: 'deprecated' }, // TODO: non-config
132
140
 
133
141
  'def-missing-element': { severity: 'Error' },
@@ -149,7 +157,7 @@ const centralMessages = {
149
157
  // For messageIds, where no text has been provided via code (central def)
150
158
  const centralMessageTexts = {
151
159
  'anno-mismatched-ellipsis': 'An array with $(CODE) can only be used if there is an assignment below with an array value',
152
- 'anno-unexpected-ellipsis': 'Unexpected $(CODE) in annotation assignment',
160
+ 'anno-unexpected-ellipsis': 'No base annotation available to apply $(CODE)',
153
161
  'missing-type-parameter': 'Missing value for type parameter $(NAME) in reference to type $(ID)',
154
162
  'syntax-csn-expected-object': 'Expected object for property $(PROP)',
155
163
  'syntax-csn-expected-column': 'Expected object or string \'*\' for property $(PROP)',
@@ -171,6 +179,22 @@ const centralMessageTexts = {
171
179
  one: 'Expected array in $(PROP) to have at least one item',
172
180
  suffix: 'With sibling property $(OTHERPROP), expected array in $(PROP) to have at least one item',
173
181
  },
182
+ 'syntax-invalid-text-block': 'Missing newline in text block',
183
+ 'syntax-unknown-escape': 'Unknown escape sequence $(CODE)',
184
+ 'syntax-invalid-escape': {
185
+ std: 'Invalid escape sequence $(CODE)',
186
+ octal: 'Octal escape sequences are not supported. Use unicode escapes instead',
187
+ whitespace: 'Unknown escape sequence: Can\'t escape whitespace',
188
+ codepoint: 'Undefined code-point for $(CODE)',
189
+ 'unicode-hex': 'Expected hexadecimal numbers for unicode escape but found $(CODE)',
190
+ 'hex-count': 'Expected $(NUMBER) hexadecimal numbers for escape sequence but found $(CODE)',
191
+ 'unicode-brace': 'Missing closing brace for unicode escape sequence',
192
+ 'language-identifier': 'Escape sequences in text-block\'s language identifier are not allowed',
193
+ },
194
+ 'syntax-missing-escape': {
195
+ std: 'Missing escape. Replace $(CODE) with $(NEWCODE)',
196
+ placeholder: 'Placeholders are not supported. Replace $(CODE) with $(NEWCODE)',
197
+ },
174
198
  'ref-undefined-def': {
175
199
  std: 'Artifact $(ART) has not been found',
176
200
  // TODO: proposal 'No definition of $(NAME) found',
@@ -289,6 +313,11 @@ const centralMessageTexts = {
289
313
  'odata-spec-violation-type': 'Expected element to have a type',
290
314
  'odata-spec-violation-property-name': 'Expected element name to be different from declaring $(KIND)',
291
315
  'odata-spec-violation-namespace': 'Expected service name not to be one of the reserved names $(NAMES)',
316
+ // Other odata/edm errors
317
+ 'odata-definition-exists': {
318
+ std: 'Entity can\'t be created due to name collision with existing definition $(NAME)',
319
+ proxy: 'No proxy entity created due to name collision with existing definition $(NAME) of kind $(KIND)'
320
+ }
292
321
  }
293
322
 
294
323
  /**
package/lib/base/model.js CHANGED
@@ -27,6 +27,7 @@ const availableBetaFlags = {
27
27
  ignoreAssocPublishingInUnion: true,
28
28
  nestedProjections: true,
29
29
  enableUniversalCsn: true,
30
+ sqlSnippets: true,
30
31
  // disabled by --beta-mode
31
32
  nestedServices: false,
32
33
  };
@@ -1,27 +1,31 @@
1
1
  'use strict'
2
2
 
3
- // Create a command line option processor and define valid commands, options and parameters.
4
- // In order to understand a command line like this:
5
- // $ node cdsc.js -x 1 --foo toXyz -y --bar-wiz bla arg1 arg2
6
- //
7
- // The following definitions should be made
8
- //
9
- // const optionProcessor = createOptionProcessor();
10
- // optionProcessor
11
- // .help(`General help text`);
12
- // .option('-x, --x-in-long-form <i>')
13
- // .option(' --foo')
14
- // optionProcessor.command('toXyz')
15
- // .help(`Help text for command "toXyz")
16
- // .option('-y --y-in-long-form')
17
- // .option(' --bar-wiz <w>', ['bla', 'foo'])
18
- //
19
- // Options *must* have a long form, can have at most one <param>, and optionally
20
- // an array of valid param values as strings. Commands and param values must not
21
- // start with '--'. The whole processor and each command may carry a help text.
22
- // To actually parse a command line, use
23
- // optionProcessor.processCmdLine(process.argv);
24
- // (see below)
3
+ /**
4
+ * Create a command line option processor and define valid commands, options and parameters.
5
+ * In order to understand a command line like this:
6
+ * $ node cdsc.js -x 1 --foo toXyz -y --bar-wiz bla arg1 arg2
7
+ *
8
+ * The following definitions should be made:
9
+ *
10
+ * ```js
11
+ * const optionProcessor = createOptionProcessor();
12
+ * optionProcessor
13
+ * .help(`General help text`);
14
+ * .option('-x, --long-form <i>')
15
+ * .option(' --foo')
16
+ * optionProcessor.command('toXyz')
17
+ * .help(`Help text for command "toXyz")
18
+ * .option('-y --y-in-long-form')
19
+ * .option(' --bar-wiz <w>', ['bla', 'foo'])
20
+ * ```
21
+ *
22
+ * Options *must* have a long form, can have at most one <param>, and optionally
23
+ * an array of valid param values as strings. Commands and param values must not
24
+ * start with '-'. The whole processor and each command may carry a help text.
25
+ * To actually parse a command line, use
26
+ * const cli = optionProcessor.processCmdLine(process.argv);
27
+ * (see below)
28
+ */
25
29
  function createOptionProcessor() {
26
30
  const optionProcessor = {
27
31
  commands: {},
@@ -32,13 +36,14 @@ function createOptionProcessor() {
32
36
  command,
33
37
  positionalArgument: (argumentDefinition) => {
34
38
  // Default positional arguments; may be overwritten by commands.
35
- _positionalArguments(argumentDefinition);
39
+ _setPositionalArguments(argumentDefinition);
36
40
  return optionProcessor;
37
41
  },
38
42
  help,
39
43
  processCmdLine,
40
44
  verifyOptions,
41
45
  camelOptionsForCommand,
46
+ // TODO: Why exported?
42
47
  _parseCommandString,
43
48
  _parseOptionString,
44
49
  }
@@ -48,9 +53,10 @@ function createOptionProcessor() {
48
53
  * API: Define a general option.
49
54
  * @param {string} optString Option string describing the command line option.
50
55
  * @param {string[]} [validValues] Array of valid values for the options.
56
+ * @param {object} [options] Further options such as `ignoreCase: true`
51
57
  */
52
- function option(optString, validValues) {
53
- return _addOption(optionProcessor, optString, validValues);
58
+ function option(optString, validValues, options) {
59
+ return _addOption(optionProcessor, optString, validValues, options);
54
60
  }
55
61
 
56
62
  /**
@@ -64,7 +70,7 @@ function createOptionProcessor() {
64
70
 
65
71
  /**
66
72
  * API: Define a command
67
- * @param {string} cmdString Command name, e.g. 'S, toSql'
73
+ * @param {string} cmdString Command name, short and long form, e.g. 'S, toSql'
68
74
  */
69
75
  function command(cmdString) {
70
76
  /** @type {object} */
@@ -73,7 +79,7 @@ function createOptionProcessor() {
73
79
  positionalArguments: [],
74
80
  option,
75
81
  positionalArgument: (argumentDefinition) => {
76
- _positionalArguments(argumentDefinition, command.positionalArguments);
82
+ _setPositionalArguments(argumentDefinition, command.positionalArguments);
77
83
  return command;
78
84
  },
79
85
  help,
@@ -92,12 +98,12 @@ function createOptionProcessor() {
92
98
  }
93
99
  return command;
94
100
 
95
- // API: Define a command option
96
- function option(optString, validValues) {
97
- return _addOption(command, optString, validValues);
101
+ // Command API: Define a command option
102
+ function option(optString, validValues, options) {
103
+ return _addOption(command, optString, validValues, options);
98
104
  }
99
105
 
100
- // API: Define the command help text
106
+ // Command API: Define the command help text
101
107
  function help(text) {
102
108
  command.helpText = text;
103
109
  return command;
@@ -112,8 +118,9 @@ function createOptionProcessor() {
112
118
  *
113
119
  * @param {string} argumentDefinition Positional arguments, e.g. '<input> <output>' or '<files...>'
114
120
  * @param {object[]} argList Array, to which the parsed arguments will be added. Default is global scope.
121
+ * @private
115
122
  */
116
- function _positionalArguments(argumentDefinition, argList = optionProcessor.positionalArguments) {
123
+ function _setPositionalArguments(argumentDefinition, argList = optionProcessor.positionalArguments) {
117
124
  if (argList.find((arg) => arg.isDynamic)) {
118
125
  throw new Error(`Can't add positional arguments after a dynamic one`);
119
126
  }
@@ -149,8 +156,10 @@ function createOptionProcessor() {
149
156
  * @private
150
157
  * @see option()
151
158
  */
152
- function _addOption(command, optString, validValues) {
159
+ function _addOption(command, optString, validValues, options) {
153
160
  const opt = _parseOptionString(optString, validValues);
161
+ Object.assign(opt, options);
162
+
154
163
  if (command.options[opt.longName]) {
155
164
  throw new Error(`Duplicate assignment for long option ${opt.longName}`);
156
165
  } else if (optionProcessor.options[opt.longName]) {
@@ -217,7 +226,6 @@ function createOptionProcessor() {
217
226
  let longName;
218
227
  let shortName;
219
228
  let param;
220
- let camelName;
221
229
 
222
230
  // split at spaces (with optional preceding comma)
223
231
  const tokens = optString.trim().split(/,? +/);
@@ -261,26 +269,16 @@ function createOptionProcessor() {
261
269
  throw new Error(`Valid values must be of type string: ${optString}`);
262
270
  });
263
271
  }
264
- camelName = _camelify(longName);
272
+
265
273
  return {
266
274
  longName,
267
275
  shortName,
268
- camelName,
276
+ camelName: camelifyLongOption(longName),
269
277
  param,
270
278
  validValues
271
279
  }
272
280
  }
273
281
 
274
- // Return a camelCase name "fooBar" for a long option "--foo-bar"
275
- function _camelify(opt) {
276
- return opt.substring(2).replace(/-./g, s => s.substring(1).toUpperCase());
277
- }
278
-
279
- // Return a long option name like "--foo-bar" for a camel-case name "fooBar"
280
- function _unCamelify(opt) {
281
- return `--${opt.replace(/[A-Z]/g, s => '-' + s.toLowerCase())}`;
282
- }
283
-
284
282
  // API: Let the option processor digest a command line 'argv'
285
283
  // The expectation is to get a commandline like this:
286
284
  // $ node cdsc.js -x 1 --foo toXyz -y --bar-wiz bla arg1 arg2
@@ -336,7 +334,12 @@ function createOptionProcessor() {
336
334
  argv = [ ...argv.slice(0, i), ...arg.split('='), ...argv.slice(i + 1)];
337
335
  arg = argv[i];
338
336
  }
339
- if (!seenDashDash && arg.startsWith('-') && arg !== '--') {
337
+
338
+ if (arg === '--') {
339
+ // No more options after '--'
340
+ seenDashDash = true;
341
+ }
342
+ else if (!seenDashDash && arg.startsWith('-')) {
340
343
  if (result.command) {
341
344
  // We already have a command
342
345
  const opt = optionProcessor.commands[result.command].options[arg];
@@ -378,10 +381,7 @@ function createOptionProcessor() {
378
381
  }
379
382
  }
380
383
  }
381
- else if (arg === '--') {
382
- // No more options after '--'
383
- seenDashDash = true;
384
- } else {
384
+ else {
385
385
  // Command or arg
386
386
  if (result.command === undefined) {
387
387
  if (optionProcessor.commands[arg]) {
@@ -420,8 +420,7 @@ function createOptionProcessor() {
420
420
  */
421
421
  function getCurrentPositionArguments() {
422
422
  const cmd = optionProcessor.commands[result.command];
423
- const args = ( cmd && cmd.positionalArguments && cmd.positionalArguments.length ) ? cmd.positionalArguments : optionProcessor.positionalArguments;
424
- return args;
423
+ return ( cmd && cmd.positionalArguments && cmd.positionalArguments.length ) ? cmd.positionalArguments : optionProcessor.positionalArguments;
425
424
  }
426
425
 
427
426
  function processPositionalArgument(argumentValue) {
@@ -478,13 +477,13 @@ function createOptionProcessor() {
478
477
  result.unknownOptions.push(`Unknown option "${argv[i]}" for the command "${command.longName}"`);
479
478
  } else {
480
479
  result.options[command][opt.camelName] = value;
481
- if (opt.validValues && !opt.validValues.includes(value)) {
480
+ if (!isValidOptionValue(opt, value)) {
482
481
  result.cmdErrors.push(`Invalid value "${value}" for option "${shortOption}${opt.longName}" - use one of [${opt.validValues}]`);
483
482
  }
484
483
  }
485
484
  } else {
486
485
  result.options[opt.camelName] = value;
487
- if (opt.validValues && !opt.validValues.some( validValue => validValue.toLowerCase() === value.toLowerCase() ) ) {
486
+ if (!isValidOptionValue(opt, value)) {
488
487
  result.errors.push(`Invalid value "${value}" for option "${shortOption}${opt.longName}" - use one of [${opt.validValues}]`);
489
488
  }
490
489
  }
@@ -545,7 +544,7 @@ function createOptionProcessor() {
545
544
  }
546
545
  // Look at each supplied option
547
546
  for (const camelName in options) {
548
- const opt = opts[_unCamelify(camelName)];
547
+ const opt = opts[uncamelifyLongOption(camelName)];
549
548
  let error;
550
549
  if (!opt) {
551
550
  // Don't report commands in top-level options
@@ -570,7 +569,7 @@ function createOptionProcessor() {
570
569
  // Parameter is required for this option
571
570
  if (typeof param === 'boolean') {
572
571
  return `Missing value for option "${prefix}${opt.camelName}"`;
573
- } else if (opt.validValues && !opt.validValues.includes(String(param))) {
572
+ } else if (!isValidOptionValue(opt, param)) {
574
573
  return `Invalid value "${param}" for option "${prefix}${opt.camelName}" - use one of [${opt.validValues}]`;
575
574
  }
576
575
  return false;
@@ -585,36 +584,65 @@ function createOptionProcessor() {
585
584
  }
586
585
  }
587
586
 
587
+ function isValidOptionValue(opt, value) {
588
+ // Explicitly convert to string, input 'value' may be boolean
589
+ value = String(value);
590
+ if (!opt.validValues || !opt.validValues.length)
591
+ return true;
592
+ if (opt.ignoreCase)
593
+ return opt.validValues.some( valid => valid.toLowerCase() === value.toLowerCase() );
594
+ return opt.validValues.includes(value);
595
+ }
596
+
588
597
  // Return an array of unique camelNames of the options for the specified command
589
598
  // If invalid command -> an empty array
590
599
  function camelOptionsForCommand(command) {
591
- if (command && optionProcessor.commands[command]) {
592
- const cmd = optionProcessor.commands[command];
593
- return [... new Set(
594
- Object.keys(cmd.options).map(optName => cmd.options[optName].camelName)
595
- )];
596
- } else {
597
- return [];
598
- }
600
+ if (!command || !optionProcessor.commands[command])
601
+ return []
602
+ const cmd = optionProcessor.commands[command];
603
+ const names = Object.keys(cmd.options).map(name => cmd.options[name].camelName);
604
+ return [...new Set(names)];
599
605
  }
600
606
  }
601
607
 
602
- // Check if 'opt' looks like a "-f" short option
608
+ /**
609
+ * Return a camelCase name "fooBar" for a long option "--foo-bar"
610
+ */
611
+ function camelifyLongOption(opt) {
612
+ return opt.substring(2).replace(/-./g, s => s.substring(1).toUpperCase());
613
+ }
614
+
615
+ /**
616
+ * Return a long option name like "--foo-bar" for a camel-case name "fooBar"
617
+ */
618
+ function uncamelifyLongOption(opt) {
619
+ return `--${opt.replace(/[A-Z]/g, s => '-' + s.toLowerCase())}`;
620
+ }
621
+
622
+ /**
623
+ * Check if 'opt' looks like a "-f" short option
624
+ */
603
625
  function isShortOption(opt) {
604
626
  return /^-[a-zA-Z?]$/.test(opt);
605
627
  }
606
628
 
607
- // Check if 'opt' looks like a "--foo-bar" long option
629
+ /**
630
+ * Check if 'opt' looks like a "--foo-bar" long option
631
+ */
608
632
  function isLongOption(opt) {
609
633
  return /^--[a-zA-Z0-9-]+$/.test(opt);
610
634
  }
611
635
 
612
- // Check if 'opt' looks like a "<foobar>" parameter
636
+ /**
637
+ * Check if 'opt' looks like a "<foobar>" parameter
638
+ */
613
639
  function isParam(opt) {
614
640
  return /^<[a-zA-Z-]+>$/.test(opt);
615
641
  }
616
642
 
617
- // Check if 'arg' looks like "<foobar...>"
643
+ /**
644
+ * Check if 'arg' looks like "<foobar...>"
645
+ */
618
646
  function isDynamicPositionalArgument(arg) {
619
647
  return /^<[a-zA-Z-]+[.]{3}>$/.test(arg);
620
648
  }
@@ -12,6 +12,8 @@
12
12
  "jsdoc/no-undefined-types": 0,
13
13
  // eslint-plugin-jsdoc warning
14
14
  "jsdoc/require-property": 0,
15
+ // most of the main functions have the normal forEachArtifact/Member signature anyway
16
+ "jsdoc/require-param-description": 0,
15
17
  // =airbnb, >eslint:
16
18
  "max-len": [ "error", {
17
19
  "code": 110,