@sap/eslint-plugin-cds 2.3.2 → 2.3.3

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 (43) hide show
  1. package/CHANGELOG.md +11 -50
  2. package/lib/api/index.js +11 -13
  3. package/lib/api/lint.d.ts +48 -0
  4. package/lib/constants.js +54 -0
  5. package/lib/index.js +44 -0
  6. package/lib/{impl/parser.js → parser.js} +2 -13
  7. package/lib/processor.js +47 -0
  8. package/lib/{impl/rules → rules}/assoc2many-ambiguous-key.js +50 -53
  9. package/lib/rules/latest-cds-version.js +42 -0
  10. package/lib/rules/min-node-version.js +47 -0
  11. package/lib/rules/no-db-keywords.js +46 -0
  12. package/lib/rules/no-dollar-prefixed-names.js +47 -0
  13. package/lib/{impl/rules → rules}/no-join-on-draft-enabled-entities.js +14 -11
  14. package/lib/rules/require-2many-oncond.js +27 -0
  15. package/lib/rules/sql-cast-suggestion.js +52 -0
  16. package/lib/rules/start-elements-lowercase.js +61 -0
  17. package/lib/rules/start-entities-uppercase.js +55 -0
  18. package/lib/{impl/rules → rules}/valid-csv-header.js +17 -9
  19. package/lib/{impl/utils → utils}/fuzzySearch.js +0 -0
  20. package/lib/utils/helpers.js +55 -0
  21. package/lib/{impl/utils → utils}/jsonc.js +0 -0
  22. package/lib/{impl/utils → utils}/model.js +107 -221
  23. package/lib/utils/ruleHelpers.js +56 -0
  24. package/lib/utils/ruleTester.js +79 -0
  25. package/lib/utils/rules.js +1033 -0
  26. package/lib/{impl/utils → utils}/validate.js +2 -18
  27. package/package.json +2 -2
  28. package/lib/impl/constants.js +0 -30
  29. package/lib/impl/index.js +0 -63
  30. package/lib/impl/processor.js +0 -23
  31. package/lib/impl/ruleFactory.js +0 -360
  32. package/lib/impl/rules/cds-compile-error.js +0 -34
  33. package/lib/impl/rules/latest-cds-version.js +0 -51
  34. package/lib/impl/rules/min-node-version.js +0 -44
  35. package/lib/impl/rules/no-db-keywords.js +0 -38
  36. package/lib/impl/rules/require-2many-oncond.js +0 -31
  37. package/lib/impl/rules/rule.hbs +0 -20
  38. package/lib/impl/rules/sql-cast-suggestion.js +0 -52
  39. package/lib/impl/rules/start-elements-lowercase.js +0 -75
  40. package/lib/impl/rules/start-entities-uppercase.js +0 -65
  41. package/lib/impl/types.d.ts +0 -48
  42. package/lib/impl/utils/helpers.js +0 -68
  43. package/lib/impl/utils/rules.js +0 -697
@@ -0,0 +1,1033 @@
1
+ /**
2
+ * @typedef { import('eslint').Linter.ConfigOverride.files } ConfigOverrideFiles
3
+ */
4
+
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const cp = require("child_process");
8
+ const cds = require("@sap/cds");
9
+
10
+ const semver = require("semver");
11
+ const path = require("path");
12
+ const { mkdirp } = require("@sap/cds/lib/utils");
13
+ const { SourceCode } = require("eslint");
14
+
15
+ const { hasDebugFlag, isValidFile, isVSCodeEditor } = require("./helpers");
16
+ const { DEFAULT_RULE_CATEGORY, DEFAULT_RULE_SEVERITY, DEFAULT_RULE_TYPE } = require("../constants");
17
+
18
+ const {
19
+ Cache,
20
+ getAST,
21
+ getLastLine,
22
+ initRootModel,
23
+ updateModel,
24
+ compileModelFromFile,
25
+ isNewConfigPath,
26
+ isFileInModel,
27
+ hasFileChanged,
28
+ getLocation,
29
+ getRange,
30
+ loadConfigPath,
31
+ } = require("./model");
32
+
33
+ const { isValidModel } = require("./validate");
34
+ const { exit } = require("process");
35
+
36
+ const JSONC = require("./jsonc");
37
+ const IS_WIN = os.platform() === "win32";
38
+ const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
39
+ const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
40
+
41
+ function doReport(cdscontext, reportDescriptor, d) {
42
+ switch (typeof reportDescriptor) {
43
+ case "string":
44
+ reportDescriptor = { message: reportDescriptor };
45
+ if (d) {
46
+ if (!reportDescriptor.loc) reportDescriptor.loc = cdscontext.cds.getLocation(d.name, d);
47
+ if (!reportDescriptor.file) reportDescriptor.file = (d.$location && d.$location.file) || "unknown.cds";
48
+ }
49
+ cdscontext.report(reportDescriptor);
50
+ break;
51
+ case "object":
52
+ if (!Array.isArray(reportDescriptor)) {
53
+ if (d) {
54
+ if (!reportDescriptor.loc) reportDescriptor.loc = getLocation(d.name, d);
55
+ if (!reportDescriptor.file) reportDescriptor.file = (d.$location && d.$location.file) || "unknown.cds";
56
+ }
57
+ cdscontext.report(reportDescriptor);
58
+ } else {
59
+ reportDescriptor.forEach((x) => {
60
+ if (typeof x === "string") {
61
+ x = { message: x };
62
+ }
63
+ if (d) {
64
+ if (!x.loc) x.loc = getLocation(d.name, d);
65
+ if (!x.file) x.file = (d.$location && d.$location.file) || "unknown.cds";
66
+ }
67
+ cdscontext.report(x);
68
+ });
69
+ }
70
+ break;
71
+ }
72
+ }
73
+
74
+ function reportCompilationErr(meta, node, cdscontext, err) {
75
+ const lint = { err: true };
76
+ const name = err.constructor.name;
77
+ if (err.messages) {
78
+ lint.message = `${name}: ${err.message}`;
79
+ // const text = err.message.split(/CDS Compilation failed\s+/, "");
80
+ err.messages.forEach((msg) => {
81
+ if (msg.severity === "Error") {
82
+ meta.severity = 2;
83
+ lint.file = msg.location.file;
84
+ lint.message = `${name}: ${msg}`;
85
+ lint.filePath = cdscontext.filePath;
86
+ return {
87
+ meta,
88
+ create: cdscontext.report(lint),
89
+ };
90
+ }
91
+ });
92
+ }
93
+ }
94
+
95
+ function reportErr(meta, node, cdscontext, err) {
96
+ const lint = { err: true };
97
+ lint.err = true;
98
+ if (hasDebugFlag() && !isVSCodeEditor()) {
99
+ lint.message = err.stack;
100
+ meta.severity = 2;
101
+ return {
102
+ meta,
103
+ create: cdscontext.report(lint),
104
+ };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Expands CDS context object with some CDS properties
110
+ * We also retrieve the file contents cached by the preprocessor
111
+ * @param {RuleContext} context
112
+ * @param {RuleNode} node
113
+ * @returns cdscontext
114
+ */
115
+ function addCDSContext(context, node) {
116
+ const filePath = (context.filePath = context.getPhysicalFilename());
117
+ const configPath = !Cache.has("test") ? loadConfigPath(filePath) : path.dirname(filePath);
118
+ let sourcecode = context.getSourceCode();
119
+ let code = sourcecode.getText(node);
120
+ if (!code) {
121
+ code = Cache.get(`file:${filePath}`);
122
+ }
123
+ if (code) {
124
+ sourcecode = new SourceCode(code, getAST(code));
125
+ }
126
+ const options = context.options;
127
+ const id = context.id;
128
+ return {
129
+ _context: context,
130
+ report: module.exports.reportProxy(context.report),
131
+ cds: module.exports.cdsProxy(cds, { code, filePath, configPath, id, options }),
132
+ id,
133
+ code,
134
+ sourcecode,
135
+ options,
136
+ filePath,
137
+ configPath,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Wrapper for ESLint's Rule creator:
143
+ * https://eslint.org/docs/developer-guide/working-with-rules
144
+ * - Must follow the ESLint prescribed convention for all rule exports
145
+ * - ESLint uses 'create' function to traverse its AST nodes
146
+ * - Since we do not work with an AST for cds models, a dummy 'Programm'
147
+ * node is used as an entry point
148
+ * - For all ESLint rules, we have two entry points for additional checks:
149
+ * 1. Before ESLint's report via context.report()
150
+ * (see getProxyReport())
151
+ * - More eslint-like API
152
+ * - More convenience re error reports
153
+ * @param {CDSRuleSpec} spec
154
+ * @returns {RuleModule}
155
+ */
156
+ function createRule(spec) {
157
+ const { meta, create } = spec;
158
+
159
+ if (!meta.type) meta.type = DEFAULT_RULE_TYPE;
160
+ if (!meta.severity) meta.severity = DEFAULT_RULE_SEVERITY;
161
+ if (meta.docs && !meta.docs.category) meta.docs.category = DEFAULT_RULE_CATEGORY;
162
+
163
+ return {
164
+ meta,
165
+ create: function (context) {
166
+ return {
167
+ Program: function (node) {
168
+ const cdscontext = addCDSContext(context, node, meta);
169
+
170
+ try {
171
+ const { report, ...ruleDescriptors } = cdscontext;
172
+ const handlers = create({ ...ruleDescriptors, report: (r) => report(r) });
173
+
174
+ // Report descriptors with fake visitor 'all'
175
+ // Used for environment rules and rules which require another compiled model
176
+ // (i.e. sql or odata)
177
+ if (handlers.all) {
178
+ let reportDescriptor = handlers.all();
179
+ doReport(cdscontext, reportDescriptor);
180
+ } else {
181
+ // Report descriptors with visitors using using `any.is()`
182
+ // https://pages.github.tools.sap/cap/docs/node.js/cds-reflect
183
+ if (cdscontext.cds.model) {
184
+ cdscontext.cds.model.forall((d) => {
185
+ for (let each in handlers) {
186
+ if (d.is(each)) {
187
+ let reportDescriptor = handlers[each](d);
188
+ doReport(cdscontext, reportDescriptor, d);
189
+ }
190
+ }
191
+ });
192
+ }
193
+ }
194
+ } catch (err) {
195
+ // Report errors in ESLint style
196
+ if (err.messages) {
197
+ // Always show model compile errors
198
+ reportCompilationErr(meta, node, cdscontext, err);
199
+ } else {
200
+ // Thrown errors are only shown on console with --debug
201
+ reportErr(meta, node, cdscontext, err);
202
+ }
203
+ }
204
+ },
205
+ };
206
+ },
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Checks whether a lint rule has been disabled by eslint-disable
212
+ * comments at a given location
213
+ * @param entry lint report
214
+ * @param cdscontext cds context object
215
+ * @param rules all availabe rules
216
+ * @returns boolean
217
+ */
218
+
219
+ function isRuleDisabled(entry, cdscontext) {
220
+ let isDisabled = false;
221
+ if (entry.loc && entry.loc.start) {
222
+ const line = entry.loc.start.line;
223
+ if (cdscontext) {
224
+ const rulesDisabled = _getDisabled(cdscontext.code, cdscontext.sourcecode, line);
225
+ let id = cdscontext.id;
226
+ if (line && id in rulesDisabled && rulesDisabled[id] === "off") {
227
+ isDisabled = true;
228
+ }
229
+ }
230
+ }
231
+ return isDisabled;
232
+ }
233
+
234
+
235
+ /**
236
+ * Turns rules "on" or "off" for given line according to eslint-disable
237
+ * comments:
238
+ * 1. Reads code string and extracts a list of comments (in order)
239
+ * 2. Initiates rulesDisabled array with all rules "on" by default
240
+ * 3. Switches rules "off" (or "on" again) based on disable comment
241
+ * @param code current code
242
+ * @param sourcecode source code object to get index from
243
+ * @param line current code line to analyze
244
+ * @returns rules dictionary with rules being either 'on' and 'off'
245
+ */
246
+ function _getDisabled(code, sourcecode, line) {
247
+ const listDisabled = [];
248
+ let { listEnvRules, listModelRules, listRules } = Cache.get("rulesInfo");
249
+ const rulesDisabled = listRules.reduce((o, key) => ({ ...o, [key]: "on" }), {});
250
+ let matches = [];
251
+ if (code) {
252
+ matches = [...code.matchAll(REGEX_COMMENTS)];
253
+ if (matches.length > 0) {
254
+ matches.forEach((match) => {
255
+ if (match) {
256
+ const index = match.index;
257
+ match = match[0];
258
+ if (match.includes("*/")) {
259
+ match = match.split("*/")[0].replace("/*", "");
260
+ } else if (match.includes("//")) {
261
+ match = match.split("//")[1];
262
+ }
263
+ if (match) {
264
+ match = match.trim();
265
+ }
266
+ ["disable", "enable"].forEach((keyword) => {
267
+ const loc = sourcecode.getLocFromIndex(index);
268
+ const disableType = match.split(" ")[0];
269
+ let disableRules = match.split(`${disableType} `)[1];
270
+ if (disableRules) {
271
+ disableRules = disableRules.split(",").map((rule) => rule.trim());
272
+ } else {
273
+ disableRules = listEnvRules.concat(listModelRules).map((rule) => `@sap/cds/${rule}`);
274
+ }
275
+ let comment = {};
276
+ if (
277
+ [`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)
278
+ ) {
279
+ if (disableType.includes("-next-line")) {
280
+ comment = {
281
+ lineComment: loc.line,
282
+ lineDisabled: loc.line + 1,
283
+ rules: disableRules,
284
+ type: keyword,
285
+ };
286
+ } else {
287
+ comment = {
288
+ lineComment: loc.line,
289
+ lineDisabled: loc.line,
290
+ rules: disableRules,
291
+ type: keyword,
292
+ };
293
+ }
294
+ if (!disableType.includes("-line")) {
295
+ comment.lineDisabled = "EOF";
296
+ }
297
+ }
298
+ listDisabled.push(comment);
299
+ });
300
+ }
301
+ });
302
+ for (let i = 0; i <= listDisabled.length - 1; i++) {
303
+ if (listDisabled[i].lineComment > line) {
304
+ break;
305
+ }
306
+ if (listDisabled[i].lineDisabled === "EOF" || listDisabled[i].lineDisabled === line) {
307
+ if (listDisabled[i].lineDisabled === "EOF") {
308
+ listDisabled[i].lineDisabled = getLastLine(code);
309
+ }
310
+ if (listDisabled[i].rules) {
311
+ listDisabled[i].rules.forEach((rule) => {
312
+ if (listDisabled[i].type === "disable") {
313
+ rulesDisabled[rule] = "off";
314
+ } else if (listDisabled[i].type === "enable") {
315
+ rulesDisabled[rule] = "on";
316
+ }
317
+ });
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return rulesDisabled;
324
+ }
325
+
326
+ module.exports = {
327
+ /**
328
+ * Gets value for a given key in allowed keys of ESLint's meta data
329
+ * @param {string} text meta object from rule
330
+ * @param {string} key key to get value for
331
+ * @returns Value for given key
332
+ */
333
+ getKeyFromMeta: function (text, key) {
334
+ const regexQuote = new RegExp(`${key}:[\\s]+[\\', \\\`, \\"]`, "gm");
335
+ const matchQuote = regexQuote.exec(text);
336
+ if (matchQuote) {
337
+ const quote = matchQuote[0].slice(-1);
338
+ const exprStart = `${key}:[\\s]+\\${quote}`;
339
+ const exprEnd = `(\\${quote},,?)`;
340
+ const regexKey = new RegExp(`${exprStart}[\\s\\S]*?${exprEnd}`, "gm");
341
+ const matchKey = regexKey.exec(text);
342
+ if (matchKey) {
343
+ const regexStart = new RegExp(`${exprStart}`, "gm");
344
+ const regexEnd = new RegExp(`${exprEnd}`, "gm");
345
+ return matchKey[0]
346
+ .replace(regexStart, "")
347
+ .replace(regexEnd, "")
348
+ .replace("fixable:", "")
349
+ .replace(/\\/gm, "")
350
+ .trim();
351
+ } else {
352
+ return "";
353
+ }
354
+ } else {
355
+ const regexBoolean = new RegExp(`${key}:[\\s]+true[\\s]?,`, "gm");
356
+ const matchBoolean = regexBoolean.exec(text);
357
+ if (matchBoolean) {
358
+ if (matchBoolean[0].includes("true,")) {
359
+ return true;
360
+ } else {
361
+ return false;
362
+ }
363
+ }
364
+ return "";
365
+ }
366
+ },
367
+
368
+ getPackageVersion: function (registry) {
369
+ let version;
370
+ let result;
371
+ try {
372
+ result = cp
373
+ .execSync(`npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`, {
374
+ cwd: process.cwd(),
375
+ shell: IS_WIN,
376
+ stdio: "pipe",
377
+ })
378
+ .toString();
379
+ } catch (err) {
380
+ console.err(`Failed to connect to ${registry} - check your connection and try again.`);
381
+ exit(0);
382
+ }
383
+ version = JSON.parse(result)["version"];
384
+ if (!version) {
385
+ console.err(`Failed to get latest plugin version from ${registry} - check your connection and try again.`);
386
+ exit(0);
387
+ }
388
+ return version;
389
+ },
390
+
391
+ /**
392
+ * Gets value for a given key in allowed keys for input of runRuleTester api
393
+ * @param {string} text test input for ruleTester
394
+ * @param {string} key key to get value for
395
+ * @returns Value for given key
396
+ */
397
+ getKeyFromTest: function (text, key) {
398
+ let result = "";
399
+ if (["root", "rule", "filename", "parser"].includes(key)) {
400
+ const regexTestKey = new RegExp(`${key}:.*$`, "gm");
401
+ const matchTestKey = regexTestKey.exec(text);
402
+ if (matchTestKey) {
403
+ const quote = matchTestKey[0].replace(",", "").slice(-1);
404
+ const regexTestValue = new RegExp(`${quote}[\\s\\S]*?(\\${quote},?)`, "gm");
405
+ const matchValue = regexTestValue.exec(matchTestKey[0]);
406
+ if (matchValue) {
407
+ const regex = new RegExp(`${quote},`, "gm");
408
+ const regex2 = new RegExp(`${quote}`, "gm");
409
+ result = matchValue[0].replace(regex, "").replace(regex2, "");
410
+ }
411
+ }
412
+ } else if (key === "errors") {
413
+ const regexTestKey = new RegExp(`${key}:.*$(([\\s]+.+)+])?`, "gm");
414
+ const matchTestKey = regexTestKey.exec(text);
415
+ if (matchTestKey) {
416
+ result = matchTestKey[0];
417
+ }
418
+ } else if (key === "data") {
419
+ const regexTestKey = new RegExp(`${key}:.*}`, "gm");
420
+ const matchTestKey = regexTestKey.exec(text);
421
+ if (matchTestKey) {
422
+ result = matchTestKey[0];
423
+ }
424
+ } else {
425
+ result = `No parameter \\'${key}\\' found in ruleTest`;
426
+ }
427
+ return result;
428
+ },
429
+
430
+ /**
431
+ * Generates overview table of all rules based on rule dictionary.
432
+ * @param ruleDict
433
+ * @param release
434
+ * @param table
435
+ * @returns Markdown table
436
+ */
437
+ genMdRules: function (ruleDict, release, table = true) {
438
+ let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
439
+ if (table) {
440
+ mdRules += `Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n`;
441
+ mdRules += `✔️ if the plugin's "recommended" configuration enables the rule\n\n`;
442
+ mdRules += `🔧 if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`;
443
+ mdRules += `💡 if problems reported by the rule are manually fixable (editor)\n\n`;
444
+ if (!release) {
445
+ mdRules += `🚧 if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`;
446
+ mdRules += "| | | | | | | |\n";
447
+ mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
448
+ } else {
449
+ mdRules += "| | | | | | | |\n";
450
+ mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
451
+ }
452
+ /* eslint-disable-next-line no-unused-vars */
453
+ Object.entries(ruleDict).forEach(([, rules]) => {
454
+ rules.forEach(function (rule) {
455
+ if (release) {
456
+ mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | | &nbsp; | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`;
457
+ } else {
458
+ mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | ${rule.construction} | &nbsp; | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
459
+ }
460
+ });
461
+ });
462
+ mdRules += "\n";
463
+ }
464
+ return mdRules;
465
+ },
466
+
467
+ /**
468
+ * Generates markdown documentation files for:
469
+ * - Overview of all rules in form of markdown table (RuleList)
470
+ * - List of all rules details in form of markdown page (Rules)
471
+ * If used internally within the @sap/eslint-plugin-cds, this
472
+ * also generates 'released' files, which only contain information
473
+ * on rules published until the currently released version.
474
+ * @param ruleDict
475
+ * @param docsPath
476
+ * @param release
477
+ */
478
+ genDocFiles: function (ruleDict, docsPath, release = false) {
479
+ let suffix = "";
480
+ if (release) {
481
+ suffix = "-released";
482
+ }
483
+ const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`);
484
+ const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`);
485
+
486
+ if (!fs.existsSync(ruleDocsPath)) {
487
+ fs.writeFileSync(ruleDocsPath, "", "utf8");
488
+ }
489
+ if (!fs.existsSync(ruleListDocsPath)) {
490
+ fs.writeFileSync(ruleListDocsPath, "", "utf8");
491
+ }
492
+ const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
493
+ const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
494
+
495
+ // Get rules table
496
+ let mdRuleList = module.exports.genMdRules(ruleDict, release, true);
497
+
498
+ // Get rule details
499
+ let mdRules = module.exports.genMdRules(ruleDict, release, false);
500
+ /* eslint-disable-next-line no-unused-vars */
501
+ Object.entries(ruleDict).forEach(([category, rules]) => {
502
+ rules.forEach(function (rule) {
503
+ mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`;
504
+ });
505
+ });
506
+
507
+ if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
508
+ fs.writeFileSync(ruleDocsPath, mdRules, "utf8");
509
+ fs.writeFileSync(ruleListDocsPath, mdRuleList, "utf8");
510
+ }
511
+ },
512
+
513
+ /**
514
+ * Generates custom rules documentation (markdown files)
515
+ * for user according to contents of:
516
+ * - Rule files
517
+ * - Test files (with valid/invalid/fixed examples)
518
+ * @param {string} projectPath
519
+ * @param {string} customRulesDir
520
+ */
521
+ async genDocs(projectPath, customRulesDir, registry, prepareRelease = false) {
522
+ let docsPath, rulePath, testPath, release;
523
+
524
+ if (!projectPath) {
525
+ docsPath = path.join(__dirname, "../../docs");
526
+ rulePath = path.join(__dirname, "../rules");
527
+ testPath = path.join(__dirname, "../../test/rules");
528
+ release = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../package.json"))).version;
529
+ } else {
530
+ docsPath = path.join(projectPath, `${customRulesDir}/docs`);
531
+ rulePath = path.join(projectPath, `${customRulesDir}/rules`);
532
+ testPath = path.join(projectPath, `${customRulesDir}/tests`);
533
+ if (!fs.existsSync(docsPath)) {
534
+ await mkdirp(docsPath);
535
+ }
536
+ if (!fs.existsSync(rulePath)) {
537
+ await mkdirp(rulePath);
538
+ }
539
+ if (!fs.existsSync(testPath)) {
540
+ await mkdirp(testPath);
541
+ }
542
+ }
543
+
544
+ if (registry) {
545
+ // Get rules (internal on artifactory)
546
+ let versionInternal;
547
+ if (!prepareRelease) {
548
+ versionInternal = module.exports.getPackageVersion(registry);
549
+ } else {
550
+ versionInternal = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../package.json"))).version;
551
+ }
552
+ if (versionInternal) {
553
+ console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`);
554
+ const ruleDictInternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionInternal);
555
+ module.exports.genDocFiles(ruleDictInternal, docsPath);
556
+ }
557
+ // Get rules released (external on npm)
558
+ const npmRegistry = "https://registry.npmjs.org";
559
+ let versionExternal;
560
+ if (!prepareRelease) {
561
+ versionExternal = module.exports.getPackageVersion(npmRegistry);
562
+ } else {
563
+ versionExternal = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../package.json"))).version;
564
+ }
565
+ if (versionExternal) {
566
+ console.log(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`);
567
+ const ruleDictExternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionExternal, release);
568
+ module.exports.genDocFiles(ruleDictExternal, docsPath, release);
569
+ }
570
+ } else {
571
+ // Get "custom" rules
572
+ const ruleDict = module.exports.getRuleDict(docsPath, rulePath, testPath);
573
+ module.exports.genDocFiles(ruleDict, docsPath);
574
+ }
575
+ console.log("Done!");
576
+ },
577
+
578
+ getRuleDict: function (docsPath, rulePath, testPath, versionRequired = "0.0.0", release = false) {
579
+ let mdRule, mdRuleSources, mdRuleContents;
580
+ let ruleDict = {};
581
+ fs.readdirSync(rulePath).filter(function (file) {
582
+ if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
583
+ const rule = path.basename(file).replace(path.extname(file), "");
584
+ const ruleTestPath = path.join(testPath, rule, "rule.test.js");
585
+
586
+ // Get rule meta information
587
+ const ruleMeta = require(path.join(rulePath, file)).meta;
588
+ const version = ruleMeta.docs.version;
589
+
590
+ if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
591
+ const details = ruleMeta.docs.description;
592
+ const category = ruleMeta.docs.category;
593
+ const fixable = ruleMeta.fixable;
594
+ const messages = ruleMeta.messages;
595
+ const recommended = ruleMeta.docs.recommended;
596
+ const suggestions = ruleMeta.hasSuggestions;
597
+
598
+ let underConstruction = "";
599
+ if (!release && semver.satisfies(version, `>${versionRequired}`)) {
600
+ underConstruction = "🚧";
601
+ console.log(` > 🚧 Rule '${rule}' still under construction.\n`);
602
+ }
603
+
604
+ let isFixable = "";
605
+ if (["code", "whitespace"].includes(fixable)) {
606
+ isFixable = "🔧";
607
+ }
608
+
609
+ let isRecommended = "";
610
+ if (recommended === true) {
611
+ isRecommended = "✔️";
612
+ }
613
+
614
+ let hasSuggestions = "";
615
+ if (suggestions === true) {
616
+ hasSuggestions = "💡";
617
+ }
618
+
619
+ const ruleDictEntry = {
620
+ name: rule,
621
+ details,
622
+ recommended: isRecommended,
623
+ fixable: isFixable,
624
+ hasSuggestions,
625
+ construction: underConstruction,
626
+ messages,
627
+ version: version,
628
+ };
629
+ mdRule = module.exports.getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
630
+ mdRuleContents = "";
631
+ if (!release && underConstruction) {
632
+ mdRuleContents += `## ${rule}\n<span class='shifted'>${underConstruction}&nbsp;&nbsp;<span class='label'>${category}</span></span>\n\n`;
633
+ } else {
634
+ mdRuleContents += `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`;
635
+ }
636
+ mdRuleContents += `### Rule Details\n${details}\n\n`;
637
+ if (mdRule) {
638
+ mdRuleContents += `### Examples\n${mdRule}\n\n`;
639
+ }
640
+ mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
641
+ mdRuleSources = `### Resources\n[Rule & Documentation source](${path
642
+ .relative(docsPath, path.join(rulePath, `${rule}.js`))
643
+ .replace(/\\/g, "/")})\n\n`;
644
+
645
+ ruleDictEntry.contents = mdRuleContents;
646
+ ruleDictEntry.sources = mdRuleSources;
647
+ if (Object.keys(ruleDict).includes(category)) {
648
+ ruleDict[category].push(ruleDictEntry);
649
+ } else {
650
+ ruleDict[category] = [
651
+ {
652
+ name: rule,
653
+ details,
654
+ recommended: isRecommended,
655
+ fixable: isFixable,
656
+ hasSuggestions,
657
+ version: version,
658
+ contents: mdRuleContents,
659
+ sources: mdRuleSources,
660
+ construction: underConstruction,
661
+ },
662
+ ];
663
+ }
664
+ }
665
+ }
666
+ });
667
+ return ruleDict;
668
+ },
669
+
670
+ getRuleExamples: function (ruleTestPath, testPath, ruleDictEntry) {
671
+ // Get rule valid/invalid tests
672
+ let mdRule = "";
673
+ if (fs.existsSync(ruleTestPath)) {
674
+ const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
675
+ const filename = module.exports.getKeyFromTest(ruleTest, "filename");
676
+ let errorsString = module.exports.getKeyFromTest(ruleTest, "errors");
677
+ const re = /(\S+):/gm;
678
+ errorsString = errorsString.replace(re, `"$&`).replace(/:/gm, '":').replace(/`/gm, '"');
679
+ const errors = JSONC.parse(`{${errorsString}}`).errors;
680
+ const valid = fs.readFileSync(path.join(testPath, ruleDictEntry.name, "valid", filename), "utf8");
681
+ let invalid = fs.readFileSync(path.join(testPath, ruleDictEntry.name, "invalid", filename), "utf8");
682
+ const insertAt = (str, sub, pos) => `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
683
+ let errorsSorted = [];
684
+ errors.forEach((err) => {
685
+ if (errorsSorted.length === 0) {
686
+ errorsSorted = [err];
687
+ } else {
688
+ const errLast = errorsSorted[errorsSorted.length - 1];
689
+ if (err.line > errLast.line) {
690
+ errorsSorted.push(err);
691
+ } else if (err.line < errLast.line) {
692
+ errorsSorted.unshift(err);
693
+ } else {
694
+ if (err.column > errLast.column) {
695
+ errorsSorted.push(err);
696
+ } else if (err.line < errLast.line) {
697
+ errorsSorted.unshift(err);
698
+ } else {
699
+ errorsSorted.push(err);
700
+ }
701
+ }
702
+ }
703
+ });
704
+ errorsSorted.reverse().forEach((err, i) => {
705
+ if (err.messageId) {
706
+ let msg = ruleDictEntry.messages[err.messageId];
707
+ let data;
708
+ if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
709
+ data = errorsSorted[i].suggestions[0].data;
710
+ }
711
+ if (data) {
712
+ Object.keys(data).forEach((d) => {
713
+ msg = msg.replace(`{{${d}}}`, data[d]);
714
+ });
715
+ }
716
+ err.message = msg;
717
+ }
718
+ const msg = err.message.replace(/"/gm, "`");
719
+ if (err.line) {
720
+ const code = invalid.split("\n");
721
+ code[err.line - 1] = insertAt(code[err.line - 1], "</i></b></span>", err.endColumn - 1);
722
+ code[err.line - 1] = insertAt(
723
+ code[err.line - 1],
724
+ `<span style="display:inline-block; position:relative; color:red; border-bottom:2pt dotted red" title="${msg}"><b><i>`,
725
+ err.column - 1
726
+ );
727
+ invalid = code.join("\n");
728
+ }
729
+ });
730
+
731
+ mdRule +=
732
+ `<span>✔️&nbsp;&nbsp; Example of ` +
733
+ `<span style="color:green">correct</span> ` +
734
+ `code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
735
+ mdRule +=
736
+ `<span>❌&nbsp;&nbsp; Example of ` +
737
+ `<span style="color:red">incorrect</span> ` +
738
+ `code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
739
+ }
740
+ return mdRule;
741
+ },
742
+
743
+ /**
744
+ * Gets all plugin rules and stores contents for later use in isRuleDisabled()
745
+ * @param {*} dirname rules dirname
746
+ * @param {*} rulename optional rule name (for single rule unit tests with RuleTester)
747
+ * @returns rule object with sources and rule lists
748
+ */
749
+ getRules: function (dirname, rulename) {
750
+ let rulesInfo;
751
+ if (Cache.has("rulesInfo")) {
752
+ rulesInfo = Cache.get("rulesInfo");
753
+ } else {
754
+ const rules = {};
755
+ const listEnvRules = [];
756
+ const listModelRules = [];
757
+
758
+ if (rulename) {
759
+ const file = `${rulename}.js`;
760
+
761
+ let rule = createRule(require(path.join(dirname, file)));
762
+ rules[rulename] = rule;
763
+
764
+ if (!listEnvRules.includes(rulename) && !isValidModel(file, "model")) {
765
+ listEnvRules.push(rulename);
766
+ }
767
+ if (!listModelRules.includes(rulename) && isValidModel(file, "model")) {
768
+ listModelRules.push(rulename);
769
+ }
770
+ } else {
771
+ fs.readdirSync(dirname).forEach((file) => {
772
+ if (path.extname(file) === ".js") {
773
+ const rulename = file.replace(".js", "");
774
+
775
+ let rule = createRule(require(path.join(dirname, file)));
776
+ rules[rulename] = rule;
777
+
778
+ if (!listEnvRules.includes(rulename) && !isValidModel(file, "model")) {
779
+ listEnvRules.push(rulename);
780
+ }
781
+ if (!listModelRules.includes(rulename) && isValidModel(file, "model")) {
782
+ listModelRules.push(rulename);
783
+ }
784
+ }
785
+ });
786
+ }
787
+ const listRules = listEnvRules.concat(listModelRules);
788
+
789
+ const recommended = Object.assign(
790
+ {},
791
+ ...Object.entries(rules)
792
+ .filter(([, v]) => v.meta.docs.recommended)
793
+ .map(([k, v]) => ({ [`@sap/cds/${k}`]: v.meta.severity }))
794
+ );
795
+
796
+ const all = Object.assign({}, ...Object.entries(rules).map(([k, v]) => ({ [`@sap/cds/${k}`]: v.meta.severity })));
797
+ rulesInfo = { sources: rules, all, recommended, listRules, listEnvRules, listModelRules };
798
+ Cache.set("rulesInfo", rulesInfo);
799
+ }
800
+ return rulesInfo;
801
+ },
802
+
803
+ // populateRules: function (context, customRulesDir) {
804
+ // const configPath = Cache.get("configpath") || "";
805
+ // // Allow for custom rules
806
+ // if (configPath) {
807
+ // let customRulesPath = path.join(Cache.get("configpath"), customRulesDir, "rules");
808
+ // let customRulesInfo;
809
+ // if (fs.existsSync(customRulesPath)) {
810
+ // customRulesInfo = module.exports.getRules(customRulesPath);
811
+ // Cache.set("rulesInfo", {
812
+ // listEnvRules: context.listEnvRules.concat(customRulesInfo.listEnvRules),
813
+ // listModelRules: context.listModelRules.concat(customRulesInfo.listModelRules),
814
+ // listRules: context.listRules.concat(customRulesInfo.listRules),
815
+ // });
816
+ // }
817
+ // }
818
+ // },
819
+
820
+ /**
821
+ * Generates proxy for `@sap/cds` object which adds:
822
+ * - Extra properties (model, environment, etc.)
823
+ * - Option to cache function calls (apply)
824
+ * @param {cds} object
825
+ * @returns Proxy for cds
826
+ */
827
+ cdsProxy: function (cds, { code, filePath, configPath, id, options }) {
828
+ const handler = {
829
+ get(target, prop, receiver) {
830
+ let value;
831
+ switch (prop) {
832
+ case "model":
833
+ value = module.exports.getModel(code, filePath, configPath, id, cds);
834
+ break;
835
+ case "environment":
836
+ value = module.exports.getEnvironment(options);
837
+ break;
838
+ case "getLocation":
839
+ value = getLocation;
840
+ break;
841
+ case "getRange":
842
+ value = getRange;
843
+ break;
844
+ default:
845
+ break;
846
+ }
847
+ if (value) {
848
+ return value;
849
+ } else {
850
+ value = Reflect.get(target, prop, receiver);
851
+ if (value && typeof value == "object") {
852
+ value = new Proxy(value, handler);
853
+ }
854
+ return value;
855
+ }
856
+ },
857
+ apply(target, thisArg, argumentsList) {
858
+ // NOTE: Possible to add caching for expensive function calls
859
+ // here as the rule set grows further
860
+ const result = Reflect.apply(target, this, argumentsList);
861
+ return result;
862
+ },
863
+ };
864
+ return new Proxy(cds, handler);
865
+ },
866
+ /**
867
+ * Generates proxy for ESLint's context object which adds caching
868
+ * @param {CDSRuleReport} ESLint's context object
869
+ * @returns {Rule.ReportDescriptor}
870
+ */
871
+ reportProxy: function (ruleReport) {
872
+ const handler = {
873
+ get(target, prop, receiver) {
874
+ const value = Reflect.get(target, prop, receiver);
875
+ if (typeof value !== "object") {
876
+ return value;
877
+ }
878
+ if (value) {
879
+ return new Proxy(value, handler);
880
+ }
881
+ return {
882
+ err: `Property ${prop} prop does not exist on object ${ruleReport}!`,
883
+ };
884
+ },
885
+ apply(target, thisArg, argumentsList) {
886
+ let report = false;
887
+ if (argumentsList.length > 0) {
888
+ argumentsList.forEach((lint) => {
889
+ if (lint) {
890
+ // Do not consider disabled content
891
+ if (!isRuleDisabled(lint, thisArg)) {
892
+ const isModelLint = lint.file && isValidFile(lint.file, "MODEL_FILES") && !lint.err;
893
+ if (isModelLint) {
894
+ if (module.exports.isDedicatedFile(lint, thisArg) && module.exports.isReportUnique(lint, "modelReports")) {
895
+ report = true;
896
+ }
897
+ }
898
+ if (!lint.loc) {
899
+ lint.loc = module.exports.addDefaultLoc();
900
+ }
901
+ if (!isModelLint && !lint.err) {
902
+ if (module.exports.isReportUnique(lint, "envReports", thisArg.configPath)) {
903
+ report = true;
904
+ }
905
+ }
906
+ if (lint.err) {
907
+ if (lint.file) {
908
+ report = true;
909
+ } else {
910
+ if (module.exports.isReportUnique(lint, "errReports", thisArg.configPath)) {
911
+ report = true;
912
+ }
913
+ }
914
+ }
915
+ if (report) {
916
+ return thisArg._context.report(lint);
917
+ }
918
+ }
919
+ }
920
+ });
921
+ }
922
+ },
923
+ };
924
+ return new Proxy(ruleReport, handler);
925
+ },
926
+
927
+ resolveFilePath: function (file) {
928
+ let fileAbs;
929
+ if (!path.isAbsolute(file)) {
930
+ fileAbs = path.join(Cache.get("configpath"), file);
931
+ } else {
932
+ fileAbs = file;
933
+ }
934
+ return fileAbs;
935
+ },
936
+
937
+ isDedicatedFile: function (lint, thisArg) {
938
+ let fileAbs = module.exports.resolveFilePath(lint.file);
939
+ return fileAbs === thisArg.filePath || lint.file === "<stdin>.cds";
940
+ },
941
+
942
+ isReportUnique: function (lint, name, uniqueness) {
943
+ let report = false;
944
+ if (!uniqueness) {
945
+ uniqueness = JSON.stringify(lint);
946
+ }
947
+ if (!Cache.has(name) && uniqueness) {
948
+ const lintString = `${uniqueness}:${JSON.stringify(lint)}`;
949
+ Cache.set(name, [lintString]);
950
+ report = true;
951
+ } else {
952
+ if (uniqueness) {
953
+ const lintMessages = Cache.has(name) ? Cache.get(name) : [];
954
+ const lintString = `${uniqueness}:${JSON.stringify(lint)}`;
955
+ if (!lintMessages.includes(lintString)) {
956
+ lintMessages.push(lintString);
957
+ Cache.set(name, lintMessages);
958
+ report = true;
959
+ }
960
+ }
961
+ }
962
+ return report;
963
+ },
964
+
965
+ addDefaultLoc: function () {
966
+ return {
967
+ start: { line: 0, column: -1 },
968
+ end: { line: 0, column: -1 },
969
+ };
970
+ },
971
+
972
+ getModel: function (code, filePath, configPath) {
973
+ let model;
974
+
975
+ if (Cache.has("test")) {
976
+ return Cache.get(`model:${configPath}`);
977
+ }
978
+
979
+ if (!Cache.has(`model:${configPath}`)) {
980
+ const isValidPluginFile = isValidFile(filePath, "FILES");
981
+
982
+ if (isValidPluginFile) {
983
+ model = initRootModel(configPath);
984
+ if (module.exports.isParkedModelFile(filePath, configPath)) {
985
+ return compileModelFromFile(code, filePath);
986
+ } else if (model) {
987
+ return model;
988
+ }
989
+ }
990
+ } else {
991
+ const isValidModelFile = isValidFile(filePath, "MODEL_FILES");
992
+
993
+ if (module.exports.isParkedModelFile(filePath, configPath)) {
994
+ return compileModelFromFile(code, filePath);
995
+ } else if (isValidModelFile) {
996
+ const hasRootChanged = isNewConfigPath(configPath);
997
+ const fileChanged = hasFileChanged(code, filePath, configPath);
998
+
999
+ if (hasRootChanged || fileChanged) {
1000
+ if (hasRootChanged) {
1001
+ Cache.remove("envReports");
1002
+ }
1003
+ Cache.remove("modelReports");
1004
+ Cache.remove("errReports");
1005
+ model = updateModel(code, filePath, configPath);
1006
+ } else {
1007
+ model = Cache.get(`model:${configPath}`);
1008
+ }
1009
+ } else {
1010
+ model = Cache.get(`model:${configPath}`);
1011
+ }
1012
+ return model;
1013
+ }
1014
+ },
1015
+
1016
+ isParkedModelFile: function (filePath, configPath) {
1017
+ const isValidModelFile = isValidFile(filePath, "MODEL_FILES");
1018
+ const isInModel = isFileInModel(filePath, configPath);
1019
+ return isValidModelFile && !isInModel;
1020
+ },
1021
+
1022
+ getEnvironment: function (options) {
1023
+ let environment;
1024
+ if (options) {
1025
+ const hasEnvTestCases = options && options[0] && options[0].environment;
1026
+ if (hasEnvTestCases) {
1027
+ environment = options[0].environment;
1028
+ }
1029
+ }
1030
+ return environment;
1031
+ },
1032
+ createRule,
1033
+ };