@sap/eslint-plugin-cds 2.1.1 → 2.3.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.
@@ -1,25 +1,25 @@
1
+ /**
2
+ * @typedef { import('eslint').Linter.ConfigOverride.files } ConfigOverrideFiles
3
+ */
4
+
1
5
  const fs = require("fs");
6
+ const os = require("os");
7
+ const cp = require("child_process");
8
+ const semver = require("semver");
2
9
  const path = require("path");
3
10
  const { mkdirp } = require("@sap/cds/lib/utils");
4
- const { getLastLine } = require("./model");
11
+ const { Cache, getLastLine } = require("./model");
5
12
 
6
13
  const JSONC = require("./jsonc");
7
- const CONSTANTS = require("../constants");
8
- const REGEX_COMMENT_START = "(/\\*|(.+)?//)\\s+eslint-";
14
+ const { categories } = require("../constants");
15
+ const IS_WIN = os.platform() === "win32";
16
+ const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
9
17
  const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
10
18
 
11
19
  module.exports = {
12
20
  /**
13
- * Returns an array of allowed file extensions
14
- * the plugin can parse of the form "*.ext"
15
- * @returns Array of file extensions
16
- */
17
- getFileExtensions: function () {
18
- return CONSTANTS.files;
19
- },
20
-
21
- /**
22
- * Turns rules on/off for given line according to eslint-disable comments:
21
+ * Turns rules "on" or "off" for given line according to eslint-disable
22
+ * comments:
23
23
  * 1. Reads code string and extracts a list of comments (in order)
24
24
  * 2. Initiates rulesDisabled array with all rules "on" by default
25
25
  * 3. Switches rules "off" (or "on" again) based on disable comment
@@ -28,92 +28,140 @@ module.exports = {
28
28
  * @param line current code line to analyze
29
29
  * @returns rules dictionary with rules being either 'on' and 'off'
30
30
  */
31
- getDisabledFromComments: function (rules, code, sourcecode, line) {
31
+ getDisabled: function (code, sourcecode, line) {
32
32
  const listDisabled = [];
33
- const rulesDisabled = rules.reduce((o, key) => ({ ...o, [key]: "on" }), {});
34
- const matches = [...code.matchAll(REGEX_COMMENTS)];
35
- if (matches.length > 0) {
36
- matches.forEach((match) => {
37
- if (match) {
38
- const index = match.index;
39
- match = match[0];
40
- if (match.includes("*/")) {
41
- match = match.split("*/")[0].replace("/*", "");
42
- } else if (match.includes("//")) {
43
- match = match.split("//")[1];
44
- }
45
- if (match) match = match.trim();
46
- ["disable", "enable"].forEach((keyword) => {
47
- const loc = sourcecode.getLocFromIndex(index);
48
- const disableType = match.split(" ")[0];
49
- let disableRules = match.split(`${disableType} `)[1];
50
- if (disableRules) {
51
- disableRules = disableRules.split(",").map((rule) => rule.trim());
52
- } else {
53
- const envRules = module.exports.getEnvRules(path.join(__dirname, "../rules"));
54
- const modelRules = module.exports.getModelRules(path.join(__dirname, "../rules"));
55
- disableRules = envRules.concat(modelRules).map((rule) => `@sap/cds/${rule}`);
33
+ let { listEnvRules, listModelRules, listRules } = Cache.get("rulesInfo");
34
+ const rulesDisabled = listRules.reduce(
35
+ (o, key) => ({ ...o, [key]: "on" }),
36
+ {}
37
+ );
38
+ let matches = [];
39
+ if (code) {
40
+ matches = [...code.matchAll(REGEX_COMMENTS)];
41
+ if (matches.length > 0) {
42
+ matches.forEach((match) => {
43
+ if (match) {
44
+ const index = match.index;
45
+ match = match[0];
46
+ if (match.includes("*/")) {
47
+ match = match.split("*/")[0].replace("/*", "");
48
+ } else if (match.includes("//")) {
49
+ match = match.split("//")[1];
50
+ }
51
+ if (match) {
52
+ match = match.trim();
56
53
  }
57
- let comment = {};
58
- if (
59
- [
60
- `eslint-${keyword}`,
61
- `eslint-${keyword}-line`,
62
- `eslint-${keyword}-next-line`,
63
- ].includes(disableType)
64
- ) {
65
- if (disableType.includes("-next-line")) {
66
- comment = {
67
- lineComment: loc.line,
68
- lineDisabled: loc.line + 1,
69
- rules: disableRules,
70
- type: keyword,
71
- };
54
+ ["disable", "enable"].forEach((keyword) => {
55
+ const loc = sourcecode.getLocFromIndex(index);
56
+ const disableType = match.split(" ")[0];
57
+ let disableRules = match.split(`${disableType} `)[1];
58
+ if (disableRules) {
59
+ disableRules = disableRules
60
+ .split(",")
61
+ .map((rule) => rule.trim());
72
62
  } else {
73
- comment = {
74
- lineComment: loc.line,
75
- lineDisabled: loc.line,
76
- rules: disableRules,
77
- type: keyword,
78
- };
63
+ disableRules = listEnvRules
64
+ .concat(listModelRules)
65
+ .map((rule) => `@sap/cds/${rule}`);
79
66
  }
80
- if (!disableType.includes("-line")) {
81
- comment.lineDisabled = "EOF";
67
+ let comment = {};
68
+ if (
69
+ [
70
+ `eslint-${keyword}`,
71
+ `eslint-${keyword}-line`,
72
+ `eslint-${keyword}-next-line`,
73
+ ].includes(disableType)
74
+ ) {
75
+ if (disableType.includes("-next-line")) {
76
+ comment = {
77
+ lineComment: loc.line,
78
+ lineDisabled: loc.line + 1,
79
+ rules: disableRules,
80
+ type: keyword,
81
+ };
82
+ } else {
83
+ comment = {
84
+ lineComment: loc.line,
85
+ lineDisabled: loc.line,
86
+ rules: disableRules,
87
+ type: keyword,
88
+ };
89
+ }
90
+ if (!disableType.includes("-line")) {
91
+ comment.lineDisabled = "EOF";
92
+ }
82
93
  }
94
+ listDisabled.push(comment);
95
+ });
96
+ }
97
+ });
98
+ for (let i = 0; i <= listDisabled.length - 1; i++) {
99
+ if (listDisabled[i].lineComment > line) {
100
+ break;
101
+ }
102
+ if (
103
+ listDisabled[i].lineDisabled === "EOF" ||
104
+ listDisabled[i].lineDisabled === line
105
+ ) {
106
+ if (listDisabled[i].lineDisabled === "EOF") {
107
+ listDisabled[i].lineDisabled = getLastLine(code);
83
108
  }
84
- listDisabled.push(comment);
85
- });
109
+ if (listDisabled[i].rules) {
110
+ listDisabled[i].rules.forEach((rule) => {
111
+ if (listDisabled[i].type === "disable") {
112
+ rulesDisabled[rule] = "off";
113
+ } else if (listDisabled[i].type === "enable") {
114
+ rulesDisabled[rule] = "on";
115
+ }
116
+ });
117
+ }
118
+ }
86
119
  }
87
- });
88
- for (let i = 0; i <= listDisabled.length - 1; i++) {
89
- if (listDisabled[i].lineComment > line) {
90
- break;
120
+ }
121
+ }
122
+ return rulesDisabled;
123
+ },
124
+
125
+ /**
126
+ * Checks whether a lint rule has been disabled by eslint-disable
127
+ * comments at a given location
128
+ * @param entry lint report
129
+ * @param cdscontext cds context object
130
+ * @param rules all availabe rules
131
+ * @returns boolean
132
+ */
133
+
134
+ isRuleDisabled: function (entry, cdscontext) {
135
+ let isDisabled = false;
136
+ if (entry.loc && entry.loc.start) {
137
+ const line = entry.loc.start.line;
138
+ if (cdscontext) {
139
+ const rulesDisabled = module.exports.getDisabled(
140
+ cdscontext.code,
141
+ cdscontext.sourcecode,
142
+ line
143
+ );
144
+ let ruleID = cdscontext.ruleID;
145
+ if (!ruleID) {
146
+ ruleID = cdscontext.id;
91
147
  }
92
148
  if (
93
- listDisabled[i].lineDisabled === "EOF" ||
94
- listDisabled[i].lineDisabled === line
149
+ line &&
150
+ ruleID in rulesDisabled &&
151
+ rulesDisabled[ruleID] === "off"
95
152
  ) {
96
- if (listDisabled[i].lineDisabled === "EOF") {
97
- listDisabled[i].lineDisabled = getLastLine(code);
98
- }
99
- if (listDisabled[i].rules) {
100
- listDisabled[i].rules.forEach((rule) => {
101
- if (listDisabled[i].type === "disable") {
102
- rulesDisabled[rule] = "off";
103
- } else if (listDisabled[i].type === "enable") {
104
- rulesDisabled[rule] = "on";
105
- }
106
- });
107
- }
153
+ isDisabled = true;
108
154
  }
109
155
  }
110
156
  }
111
- return rulesDisabled;
157
+ return isDisabled;
112
158
  },
159
+
113
160
  /**
114
- * Gets value for a given key in allowed keys of ESLint's meta data in createRule api
115
- * @param {*} text
116
- * @param {*} key
161
+ * Gets value for a given key in allowed keys of ESLint's meta data in
162
+ * defineRule api
163
+ * @param {string} text meta object from rule
164
+ * @param {string} key key to get value for
117
165
  * @returns Value for given key
118
166
  */
119
167
  getKeyFromMeta: function (text, key) {
@@ -138,14 +186,44 @@ module.exports = {
138
186
  return "";
139
187
  }
140
188
  } else {
189
+ const regexBoolean = new RegExp(`${key}:[\\s]+true[\\s]?,`, "gm");
190
+ const matchBoolean = regexBoolean.exec(text);
191
+ if (matchBoolean) {
192
+ if (matchBoolean[0].includes("true,")) {
193
+ return true;
194
+ } else {
195
+ return false;
196
+ }
197
+ }
141
198
  return "";
142
199
  }
143
200
  },
144
201
 
202
+ getPackageVersion: function (registry) {
203
+ let version;
204
+ try {
205
+ const result = cp.execSync(
206
+ `npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`,
207
+ {
208
+ cwd: process.cwd(),
209
+ shell: IS_WIN,
210
+ stdio: "pipe",
211
+ })
212
+ .toString();
213
+ version = JSON.parse(result)["version"];
214
+ } catch (err) {
215
+ // Do not throw
216
+ }
217
+ if (!version) {
218
+ console.err(`Failed to get latest plugin version from ${registry} - check your connection and try again.`);
219
+ }
220
+ return version;
221
+ },
222
+
145
223
  /**
146
224
  * Gets value for a given key in allowed keys for input of runRuleTester api
147
- * @param {*} text
148
- * @param {*} key
225
+ * @param {string} text test input for ruleTester
226
+ * @param {string} key key to get value for
149
227
  * @returns Value for given key
150
228
  */
151
229
  getKeyFromTest: function (text, key) {
@@ -172,6 +250,12 @@ module.exports = {
172
250
  if (matchTestKey) {
173
251
  result = matchTestKey[0];
174
252
  }
253
+ } else if (key === "data") {
254
+ const regexTestKey = new RegExp(`${key}:.*}`, "gm");
255
+ const matchTestKey = regexTestKey.exec(text);
256
+ if (matchTestKey) {
257
+ result = matchTestKey[0];
258
+ }
175
259
  } else {
176
260
  result = `No parameter \\'${key}\\' found in ruleTest`;
177
261
  }
@@ -180,26 +264,37 @@ module.exports = {
180
264
 
181
265
  /**
182
266
  * Generates overview table of all rules based on rule dictionary.
183
- * @param {*} ruleDict
184
- * @param {*} release
185
- * @param {*} table
267
+ * @param ruleDict
268
+ * @param release
269
+ * @param table
186
270
  * @returns Markdown table
187
271
  */
188
272
  genMdRules: function (ruleDict, release, table = true) {
189
- let version = "latest";
190
- if (release) {
191
- version = release;
192
- }
193
- let mdRules = `# @sap/eslint-plugin-cds [${version}]\n\n`;
273
+ let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
194
274
  if (table) {
195
- mdRules += "<table>";
196
- mdRules += "<tr><th>Rule</th><th>Category</th><th>Recommended</th><th>Fixable</th><th>Version</th></tr>";
197
- Object.entries(ruleDict).forEach(([category, rules]) => {
275
+ mdRules += `Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n`;
276
+ mdRules += `✔️ if the plugin's "recommended" configuration enables the rule\n\n`;
277
+ mdRules += `🔧 if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`;
278
+ mdRules += `💡 if problems reported by the rule are manually fixable (editor)\n\n`;
279
+ if (!release) {
280
+ mdRules += `🚧 if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`;
281
+ mdRules += "| | | | | | | |\n";
282
+ mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
283
+ } else {
284
+ mdRules += "| | | | | | | |\n";
285
+ mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
286
+ }
287
+ /* eslint-disable-next-line no-unused-vars */
288
+ Object.entries(ruleDict).forEach(([, rules]) => {
198
289
  rules.forEach(function (rule) {
199
- mdRules += `<tr><td><a title="${rule.details}" href="./Rules.md#rule-${rule.name}">${rule.name}</a></td><td>${category}</td><td>${rule.recommended}</td><td>${rule.fix}</td><td>${rule.version}</td></tr>`;
290
+ if (release) {
291
+ mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | | &nbsp; | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`;
292
+ } else {
293
+ mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | ${rule.construction} | &nbsp; | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
294
+ }
200
295
  });
201
296
  });
202
- mdRules += "</table>\n\n<br>\n\n";
297
+ mdRules += "\n";
203
298
  }
204
299
  return mdRules;
205
300
  },
@@ -211,11 +306,11 @@ module.exports = {
211
306
  * If used internally within the @sap/eslint-plugin-cds, this
212
307
  * also generates 'released' files, which only contain information
213
308
  * on rules published until the currently released version.
214
- * @param {*} ruleDict
215
- * @param {*} docsPath
216
- * @param {*} release
309
+ * @param ruleDict
310
+ * @param docsPath
311
+ * @param release
217
312
  */
218
- genDocFiles: function (ruleDict, docsPath, release) {
313
+ genDocFiles: function (ruleDict, docsPath, release=false) {
219
314
  let suffix = "";
220
315
  if (release) {
221
316
  suffix = "-released";
@@ -232,8 +327,11 @@ module.exports = {
232
327
  const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
233
328
  const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
234
329
 
330
+ // Get rules table
331
+ let mdRuleList = module.exports.genMdRules(ruleDict, release, true);
332
+
333
+ // Get rule details
235
334
  let mdRules = module.exports.genMdRules(ruleDict, release, false);
236
- let mdRuleList = module.exports.genMdRules(ruleDict);
237
335
  /* eslint-disable-next-line no-unused-vars */
238
336
  Object.entries(ruleDict).forEach(([category, rules]) => {
239
337
  rules.forEach(function (rule) {
@@ -252,10 +350,10 @@ module.exports = {
252
350
  * for user according to contents of:
253
351
  * - Rule files
254
352
  * - Test files (with valid/invalid/fixed examples)
255
- * @param {*} projectPath
353
+ * @param {string} projectPath
354
+ * @param {string} customRulesDir
256
355
  */
257
- async genDocs(projectPath, customRulesDir) {
258
- let mdRule, mdRuleSources, mdRuleContents;
356
+ async genDocs(projectPath, customRulesDir, registry, prepareRelease = false) {
259
357
  let docsPath, rulePath, testPath, release;
260
358
 
261
359
  if (!projectPath) {
@@ -269,19 +367,58 @@ module.exports = {
269
367
  docsPath = path.join(projectPath, `${customRulesDir}/docs`);
270
368
  rulePath = path.join(projectPath, `${customRulesDir}/rules`);
271
369
  testPath = path.join(projectPath, `${customRulesDir}/tests`);
370
+ if (!fs.existsSync(docsPath)) {
371
+ await mkdirp(docsPath);
372
+ }
373
+ if (!fs.existsSync(rulePath)) {
374
+ await mkdirp(rulePath);
375
+ }
376
+ if (!fs.existsSync(testPath)) {
377
+ await mkdirp(testPath);
378
+ }
272
379
  }
273
380
 
274
- if (!fs.existsSync(docsPath)) {
275
- await mkdirp(docsPath);
276
- }
277
- if (!fs.existsSync(rulePath)) {
278
- await mkdirp(rulePath);
279
- }
280
- if (!fs.existsSync(testPath)) {
281
- await mkdirp(testPath);
381
+ if (registry) {
382
+ // Get rules (internal on artifactory)
383
+ let versionInternal;
384
+ if (!prepareRelease) {
385
+ versionInternal = module.exports.getPackageVersion(registry);
386
+ } else {
387
+ versionInternal = JSON.parse(
388
+ fs.readFileSync(path.join(__dirname, "../../../package.json"))
389
+ ).version;
390
+ }
391
+ if (versionInternal) {
392
+ console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`);
393
+ const ruleDictInternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionInternal);
394
+ module.exports.genDocFiles(ruleDictInternal, docsPath);
395
+ }
396
+ // Get rules released (external on npm)
397
+ const npmRegistry = "https://registry.npmjs.org";
398
+ let versionExternal;
399
+ if (!prepareRelease) {
400
+ versionExternal = module.exports.getPackageVersion(npmRegistry);
401
+ } else {
402
+ versionExternal = JSON.parse(
403
+ fs.readFileSync(path.join(__dirname, "../../../package.json"))
404
+ ).version;
405
+ }
406
+ if (versionExternal) {
407
+ console.log(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`);
408
+ const ruleDictExternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionExternal, release);
409
+ module.exports.genDocFiles(ruleDictExternal, docsPath, release);
410
+ }
411
+ } else {
412
+ // Get "custom" rules
413
+ const ruleDict = module.exports.getRuleDict(docsPath, rulePath, testPath);
414
+ module.exports.genDocFiles(ruleDict, docsPath);
282
415
  }
416
+ console.log('Done!')
417
+ },
283
418
 
284
- const ruleDict = {};
419
+ getRuleDict: function (docsPath, rulePath, testPath, versionRequired='0.0.0', release=false) {
420
+ let mdRule, mdRuleSources, mdRuleContents;
421
+ let ruleDict = {};
285
422
  fs.readdirSync(rulePath).filter(function (file) {
286
423
  if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
287
424
  const rule = path.basename(file).replace(path.extname(file), "");
@@ -289,169 +426,272 @@ module.exports = {
289
426
  const ruleTestPath = path.join(testPath, rule, "rule.test.js");
290
427
 
291
428
  // Get rule meta information
292
- const ruleMeta = fs.readFileSync(path.join(rulePath, file), "utf8");
293
- const details = module.exports.getKeyFromMeta(
294
- ruleMeta,
295
- "description"
296
- );
297
- const category = module.exports.getKeyFromMeta(ruleMeta, "category");
298
- const fixable = module.exports.getKeyFromMeta(ruleMeta, "fixable");
299
-
300
- let isFixable = "";
301
- if (["code", "whitespace"].includes(fixable)) {
302
- isFixable = "🔧";
303
- }
429
+ const ruleMeta = require(path.join(rulePath, file)).meta;
430
+ const version = ruleMeta.docs.version;
304
431
 
305
- let isRecommended = "";
306
- if (Object.keys(CONSTANTS.recommended).includes(`@sap/cds/${rule}`)) {
307
- isRecommended = "✔️";
308
- }
432
+ if ((release && semver.satisfies(version, `<=${versionRequired}`))
433
+ || (!release)) {
434
+ const details = ruleMeta.docs.description;
435
+ const category = ruleMeta.docs.category;
436
+ const fixable = ruleMeta.fixable;
437
+ const messages = ruleMeta.messages;
438
+ const recommended = ruleMeta.docs.recommended;
439
+ const suggestions = ruleMeta.hasSuggestions;
309
440
 
310
- const version = module.exports.getKeyFromMeta(ruleMeta, "version");
441
+ let underConstruction = "";
442
+ if (!release && semver.satisfies(version, `>${versionRequired}`)) {
443
+ underConstruction = "🚧";
444
+ console.log(` > 🚧 Rule '${rule}' still under construction.\n`)
445
+ }
311
446
 
312
- // Get rule valid/invalid tests
313
- mdRule = "";
314
- if (fs.existsSync(ruleTestPath)) {
315
- const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
316
- const filename = module.exports.getKeyFromTest(
317
- ruleTest,
318
- "filename"
319
- );
320
- let errorsString = module.exports.getKeyFromTest(
321
- ruleTest,
322
- "errors"
323
- );
324
- const re = /(\S+):/gm;
325
- errorsString = errorsString
326
- .replace(re, `"$&`)
327
- .replace(/:/gm, '":')
328
- .replace(/`/gm, '"');
329
- const errors = JSONC.parse(`{${errorsString}}`).errors;
330
- const valid = fs.readFileSync(
331
- path.join(testPath, rule, "valid", filename),
332
- "utf8"
333
- );
334
- let invalid = fs.readFileSync(
335
- path.join(testPath, rule, "invalid", filename),
336
- "utf8"
337
- );
338
- const insertAt = (str, sub, pos) =>
339
- `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
340
- errors.forEach((err) => {
341
- const msg = err.message.replace(/"/gm, "`");
342
- if (err.line) {
343
- const code = invalid.split("\n");
344
- code[err.line - 1] = insertAt(
345
- code[err.line - 1],
346
- "</span>",
347
- err.endColumn - 1
348
- );
349
- code[err.line - 1] = insertAt(
350
- code[err.line - 1],
351
- `<span style="text-decoration-line:underline; text-decoration-style:wavy; text-decoration-color:red;" title="${msg}">`,
352
- err.column - 1
353
- );
354
- invalid = code.join("\n");
355
- }
356
- });
447
+ let isFixable = "";
448
+ if (["code", "whitespace"].includes(fixable)) {
449
+ isFixable = "🔧";
450
+ }
357
451
 
358
- mdRule +=
359
- `<span>✔️&nbsp;&nbsp; Example of ` +
360
- `<span style="color:green">correct</span> ` +
361
- `code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
362
- mdRule +=
363
- `<span>❌&nbsp;&nbsp; Example of ` +
364
- `<span style="color:red">incorrect</span> ` +
365
- `code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
366
- }
452
+ let isRecommended = "";
453
+ if (recommended === true) {
454
+ isRecommended = "✔️";
455
+ }
367
456
 
368
- mdRuleContents = `## Rule: ${rule}\n`;
369
- mdRuleContents += `<span class='label shifted'>${category}</span>\n\n`;
370
- mdRuleContents += `### Rule Details\n${details}\n\n`;
371
- if (mdRule) {
372
- mdRuleContents += `### Examples\n${mdRule}\n\n`;
373
- }
374
- mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
375
- mdRuleSources = `### Resources\n[Rule & Documentation source](${path.relative(
376
- docsPath,
377
- path.join(rulePath, `${rule}.js`)
378
- ).replace(/\\/g,'/')})\n\n`;
457
+ let hasSuggestions = "";
458
+ if (suggestions === true) {
459
+ hasSuggestions = "💡";
460
+ }
379
461
 
380
- if (Object.keys(ruleDict).includes(category)) {
381
- ruleDict[category].push({
462
+ const ruleDictEntry = {
382
463
  name: rule,
383
464
  details,
384
465
  recommended: isRecommended,
385
- fix: isFixable,
466
+ fixable: isFixable,
467
+ hasSuggestions,
468
+ construction: underConstruction,
469
+ messages,
386
470
  version: version,
387
- contents: mdRuleContents,
388
- sources: mdRuleSources,
389
- });
390
- } else {
391
- ruleDict[category] = [
392
- {
393
- name: rule,
394
- details,
395
- recommended: isRecommended,
396
- fix: isFixable,
397
- version: version,
398
- contents: mdRuleContents,
399
- sources: mdRuleSources,
400
- },
401
- ];
471
+ };
472
+ mdRule = module.exports.getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
473
+ mdRuleContents = "";
474
+ if (!release && underConstruction) {
475
+ mdRuleContents += `## ${rule}\n<span class='shifted'>${underConstruction}&nbsp;&nbsp;<span class='label'>${category}</span></span>\n\n`;
476
+ } else {
477
+ mdRuleContents += `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`;
478
+ }
479
+ mdRuleContents += `### Rule Details\n${details}\n\n`;
480
+ if (mdRule) {
481
+ mdRuleContents += `### Examples\n${mdRule}\n\n`;
482
+ }
483
+ mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
484
+ mdRuleSources = `### Resources\n[Rule & Documentation source](${path
485
+ .relative(docsPath, path.join(rulePath, `${rule}.js`))
486
+ .replace(/\\/g, "/")})\n\n`;
487
+
488
+ ruleDictEntry.contents = mdRuleContents;
489
+ ruleDictEntry.sources = mdRuleSources;
490
+ if (Object.keys(ruleDict).includes(category)) {
491
+ ruleDict[category].push(ruleDictEntry);
492
+ } else {
493
+ ruleDict[category] = [
494
+ {
495
+ name: rule,
496
+ details,
497
+ recommended: isRecommended,
498
+ fixable: isFixable,
499
+ hasSuggestions,
500
+ version: version,
501
+ contents: mdRuleContents,
502
+ sources: mdRuleSources,
503
+ construction: underConstruction
504
+ },
505
+ ];
506
+ }
402
507
  }
403
508
  }
404
509
  }
405
510
  });
511
+ return ruleDict;
512
+ },
406
513
 
407
- module.exports.genDocFiles(ruleDict, docsPath);
408
- if (release) {
409
- module.exports.genDocFiles(ruleDict, docsPath, release);
514
+ getRuleExamples: function (ruleTestPath, testPath, ruleDictEntry) {
515
+ // Get rule valid/invalid tests
516
+ let mdRule = "";
517
+ if (fs.existsSync(ruleTestPath)) {
518
+ const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
519
+ const filename = module.exports.getKeyFromTest(
520
+ ruleTest,
521
+ "filename"
522
+ );
523
+ let errorsString = module.exports.getKeyFromTest(
524
+ ruleTest,
525
+ "errors"
526
+ );
527
+ const re = /(\S+):/gm;
528
+ errorsString = errorsString
529
+ .replace(re, `"$&`)
530
+ .replace(/:/gm, '":')
531
+ .replace(/`/gm, '"');
532
+ const errors = JSONC.parse(`{${errorsString}}`).errors;
533
+ const valid = fs.readFileSync(
534
+ path.join(testPath, ruleDictEntry.name, "valid", filename),
535
+ "utf8"
536
+ );
537
+ let invalid = fs.readFileSync(
538
+ path.join(testPath, ruleDictEntry.name, "invalid", filename),
539
+ "utf8"
540
+ );
541
+ const insertAt = (str, sub, pos) =>
542
+ `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
543
+ let errorsSorted = []
544
+ errors.forEach((err) => {
545
+ if (errorsSorted.length === 0) {
546
+ errorsSorted = [err];
547
+ } else {
548
+ const errLast = errorsSorted[errorsSorted.length - 1];
549
+ if (err.line > errLast.line) {
550
+ errorsSorted.push(err)
551
+ } else if (err.line < errLast.line) {
552
+ errorsSorted.unshift(err);
553
+ } else {
554
+ if (err.column > errLast.column) {
555
+ errorsSorted.push(err)
556
+ } else if (err.line < errLast.line) {
557
+ errorsSorted.unshift(err)
558
+ } else {
559
+ errorsSorted.push(err)
560
+ }
561
+ }
562
+ }
563
+ })
564
+ errorsSorted.reverse().forEach((err, i) => {
565
+ if (err.messageId) {
566
+ let msg = ruleDictEntry.messages[err.messageId];
567
+ let data;
568
+ if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
569
+ data = errorsSorted[i].suggestions[0].data;
570
+ }
571
+ if (data) {
572
+ Object.keys(data).forEach((d) => {
573
+ msg = msg.replace(`{{${d}}}`, data[d]);
574
+ })
575
+ }
576
+ err.message = msg;
577
+ }
578
+ const msg = err.message.replace(/"/gm, "`");
579
+ if (err.line) {
580
+ const code = invalid.split("\n");
581
+ code[err.line - 1] = insertAt(
582
+ code[err.line - 1],
583
+ "</span>",
584
+ err.endColumn - 1
585
+ );
586
+ code[err.line - 1] = insertAt(
587
+ code[err.line - 1],
588
+ `<span style="display:inline-block; position:relative; border-bottom:2pt dotted red" title="${msg}">`,
589
+ err.column - 1
590
+ );
591
+ invalid = code.join("\n");
592
+ }
593
+ });
594
+
595
+ mdRule +=
596
+ `<span>✔️&nbsp;&nbsp; Example of ` +
597
+ `<span style="color:green">correct</span> ` +
598
+ `code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
599
+ mdRule +=
600
+ `<span>❌&nbsp;&nbsp; Example of ` +
601
+ `<span style="color:red">incorrect</span> ` +
602
+ `code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
410
603
  }
604
+ return mdRule;
411
605
  },
412
606
 
413
- getRules: function (dirname) {
607
+ /**
608
+ * Gets all relevant rules information (dictionary of rules contents, lists
609
+ * of 'env' and 'model' rules) for the rules directory provided
610
+ * - Default categories is 'model'
611
+ * @param {string} dirname
612
+ * @returns rules information
613
+ */
614
+ getRules(dirname) {
414
615
  const rules = {};
616
+ const listEnvRules = [];
617
+ const listModelRules = [];
415
618
  fs.readdirSync(dirname).forEach((file) => {
416
619
  if (path.extname(file) === ".js" && file !== "index.js") {
417
- rules[file.replace(".js", "")] = require(path.join(dirname, file));
620
+ const rulename = file.replace(".js", "");
621
+ let rule = require(path.join(dirname, file));
622
+ if (!rule.meta) {
623
+ return;
624
+ }
625
+ rule = module.exports.applyRuleDefaults(rule);
626
+ rules[rulename] = rule;
627
+ const category =
628
+ rules[rulename].meta.docs.category || categories["model"];
629
+ if (
630
+ !listEnvRules.includes(rulename) &&
631
+ category === categories["env"]
632
+ ) {
633
+ listEnvRules.push(rulename);
634
+ }
635
+ if (
636
+ !listModelRules.includes(rulename) &&
637
+ category === categories["model"]
638
+ ) {
639
+ listModelRules.push(rulename);
640
+ }
418
641
  }
419
642
  });
420
- return rules;
643
+ const listRules = listEnvRules.concat(listModelRules);
644
+ return { rules, listRules, listEnvRules, listModelRules };
421
645
  },
422
646
 
423
- // REVISIT: The two methods below...
424
- // 1. Read all rule implementations .js sources and scan them for meta data -> why not using these files' module exports?
425
- // 2. They do so twice per file -> once for getEnvRules, once for getModelRules
426
- // 3. Both are even invoked twice by `cds lint` -> once before `eslint` is spawned, once after
427
-
428
- getEnvRules: function (dirname) {
429
- const envRules = [];
430
- fs.readdirSync(dirname).forEach((file) => {
431
- if (path.extname(file) === ".js" && file !== "index.js") {
432
- const rulePath = path.join(dirname, file);
433
- const ruleMeta = fs.readFileSync(rulePath, "utf8");
434
- const category = module.exports.getKeyFromMeta(ruleMeta, "category");
435
- if (category === "Environment") {
436
- envRules.push(file.replace(".js", ""));
437
- }
647
+ /**
648
+ * Sets defaults for rule meta data for missing required
649
+ * propeties:
650
+ * - Rule is of type "problem"
651
+ * - Rule is in model category
652
+ * - Rule severity is "error"
653
+ * @param {*} rule
654
+ * @returns
655
+ */
656
+ applyRuleDefaults(rule) {
657
+ let ruleSanitized;
658
+ if (rule.meta) {
659
+ ruleSanitized = { ...rule };
660
+ if (!rule.meta.type) {
661
+ ruleSanitized.meta.type = "problem";
438
662
  }
439
- });
440
- return envRules;
663
+ if (rule.meta.docs && !rule.meta.docs.category) {
664
+ ruleSanitized.meta.docs.category = categories["model"];
665
+ }
666
+ if (rule.meta.docs.recommended && !rule.meta.severity) {
667
+ ruleSanitized.meta.severity = "error";
668
+ }
669
+ }
670
+ return ruleSanitized;
441
671
  },
442
672
 
443
- getModelRules: function (dirname) {
444
- const modelRules = [];
445
- fs.readdirSync(dirname).forEach((file) => {
446
- if (path.extname(file) === ".js" && file !== "index.js") {
447
- const rulePath = path.join(dirname, file);
448
- const ruleMeta = fs.readFileSync(rulePath, "utf8");
449
- const category = module.exports.getKeyFromMeta(ruleMeta, "category");
450
- if (category === CONSTANTS.categories.model) {
451
- modelRules.push(file.replace(".js", ""));
452
- }
673
+ populateRules: function (context, customRulesDir) {
674
+ const configPath = Cache.get("configpath") || "";
675
+ // Allow for custom rules
676
+ if (configPath) {
677
+ let customRulesPath = path.join(
678
+ Cache.get("configpath"),
679
+ customRulesDir,
680
+ "rules"
681
+ );
682
+ let customRulesInfo;
683
+ if (fs.existsSync(customRulesPath)) {
684
+ customRulesInfo = module.exports.getRules(customRulesPath);
685
+ Cache.set("rulesInfo", {
686
+ listEnvRules: context.listEnvRules.concat(
687
+ customRulesInfo.listEnvRules
688
+ ),
689
+ listModelRules: context.listModelRules.concat(
690
+ customRulesInfo.listModelRules
691
+ ),
692
+ listRules: context.listRules.concat(customRulesInfo.listRules),
693
+ });
453
694
  }
454
- });
455
- return modelRules;
695
+ }
456
696
  },
457
697
  };