@sap/eslint-plugin-cds 2.0.4 → 2.2.1

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.
@@ -0,0 +1,541 @@
1
+ /**
2
+ * @typedef { import('eslint').Linter.ConfigOverride.files } ConfigOverrideFiles
3
+ */
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { mkdirp } = require("@sap/cds/lib/utils");
8
+ const { getLastLine } = require("./model");
9
+
10
+ const JSONC = require("./jsonc");
11
+ const { categories, files, recommended } = require("../constants");
12
+ const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
13
+ const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
14
+
15
+ module.exports = {
16
+ /**
17
+ * Returns an array of allowed file extensions
18
+ * the plugin can parse of the form "*.ext"
19
+ * @returns {ConfigOverrideFiles} Array of file extensions
20
+ */
21
+ getFileExtensions: function () {
22
+ return files;
23
+ },
24
+
25
+ /**
26
+ * Turns rules "on" or "off" for given line according to eslint-disable
27
+ * comments:
28
+ * 1. Reads code string and extracts a list of comments (in order)
29
+ * 2. Initiates rulesDisabled array with all rules "on" by default
30
+ * 3. Switches rules "off" (or "on" again) based on disable comment
31
+ * @param code current code
32
+ * @param sourcecode source code object to get index from
33
+ * @param line current code line to analyze
34
+ * @returns rules dictionary with rules being either 'on' and 'off'
35
+ */
36
+ getDisabled: function (
37
+ rules,
38
+ listEnvRules,
39
+ listModelRules,
40
+ code,
41
+ sourcecode,
42
+ line
43
+ ) {
44
+ const listDisabled = [];
45
+ const rulesDisabled = rules.reduce((o, key) => ({ ...o, [key]: "on" }), {});
46
+ let matches = [];
47
+ if (code) {
48
+ matches = [...code.matchAll(REGEX_COMMENTS)];
49
+ if (matches.length > 0) {
50
+ matches.forEach((match) => {
51
+ if (match) {
52
+ const index = match.index;
53
+ match = match[0];
54
+ if (match.includes("*/")) {
55
+ match = match.split("*/")[0].replace("/*", "");
56
+ } else if (match.includes("//")) {
57
+ match = match.split("//")[1];
58
+ }
59
+ if (match) {
60
+ match = match.trim();
61
+ }
62
+ ["disable", "enable"].forEach((keyword) => {
63
+ const loc = sourcecode.getLocFromIndex(index);
64
+ const disableType = match.split(" ")[0];
65
+ let disableRules = match.split(`${disableType} `)[1];
66
+ if (disableRules) {
67
+ disableRules = disableRules
68
+ .split(",")
69
+ .map((rule) => rule.trim());
70
+ } else {
71
+ disableRules = listEnvRules
72
+ .concat(listModelRules)
73
+ .map((rule) => `@sap/cds/${rule}`);
74
+ }
75
+ let comment = {};
76
+ if (
77
+ [
78
+ `eslint-${keyword}`,
79
+ `eslint-${keyword}-line`,
80
+ `eslint-${keyword}-next-line`,
81
+ ].includes(disableType)
82
+ ) {
83
+ if (disableType.includes("-next-line")) {
84
+ comment = {
85
+ lineComment: loc.line,
86
+ lineDisabled: loc.line + 1,
87
+ rules: disableRules,
88
+ type: keyword,
89
+ };
90
+ } else {
91
+ comment = {
92
+ lineComment: loc.line,
93
+ lineDisabled: loc.line,
94
+ rules: disableRules,
95
+ type: keyword,
96
+ };
97
+ }
98
+ if (!disableType.includes("-line")) {
99
+ comment.lineDisabled = "EOF";
100
+ }
101
+ }
102
+ listDisabled.push(comment);
103
+ });
104
+ }
105
+ });
106
+ for (let i = 0; i <= listDisabled.length - 1; i++) {
107
+ if (listDisabled[i].lineComment > line) {
108
+ break;
109
+ }
110
+ if (
111
+ listDisabled[i].lineDisabled === "EOF" ||
112
+ listDisabled[i].lineDisabled === line
113
+ ) {
114
+ if (listDisabled[i].lineDisabled === "EOF") {
115
+ listDisabled[i].lineDisabled = getLastLine(code);
116
+ }
117
+ if (listDisabled[i].rules) {
118
+ listDisabled[i].rules.forEach((rule) => {
119
+ if (listDisabled[i].type === "disable") {
120
+ rulesDisabled[rule] = "off";
121
+ } else if (listDisabled[i].type === "enable") {
122
+ rulesDisabled[rule] = "on";
123
+ }
124
+ });
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return rulesDisabled;
131
+ },
132
+
133
+ /**
134
+ * Checks whether a given lint error/warning has been disabled
135
+ * by comments (as it should then be suppressed)
136
+ * @param entry lint report
137
+ * @param cdscontext cds context object
138
+ * @param rules all availabe rules
139
+ * @returns boolean
140
+ */
141
+
142
+ checkDisabled: function (
143
+ entry,
144
+ cdscontext,
145
+ rules,
146
+ listEnvRules,
147
+ listModelRules
148
+ ) {
149
+ let isDisabled = false;
150
+ if (entry.loc && entry.loc.start) {
151
+ const line = entry.loc.start.line;
152
+ if (cdscontext) {
153
+ const rulesDisabled = module.exports.getDisabled(
154
+ rules,
155
+ listEnvRules,
156
+ listModelRules,
157
+ cdscontext.code,
158
+ cdscontext.sourcecode,
159
+ line
160
+ );
161
+ let ruleID = cdscontext.ruleID;
162
+ if (!ruleID) {
163
+ ruleID = cdscontext.id;
164
+ }
165
+ if (
166
+ line &&
167
+ ruleID in rulesDisabled &&
168
+ rulesDisabled[ruleID] === "off"
169
+ ) {
170
+ isDisabled = true;
171
+ }
172
+ }
173
+ }
174
+ return isDisabled;
175
+ },
176
+
177
+ /**
178
+ * Gets value for a given key in allowed keys of ESLint's meta data in
179
+ * defineRule api
180
+ * @param {string} text meta object from rule
181
+ * @param {string} key key to get value for
182
+ * @returns Value for given key
183
+ */
184
+ getKeyFromMeta: function (text, key) {
185
+ const regexQuote = new RegExp(`${key}:[\\s]+[\\', \\\`, \\"]`, "gm");
186
+ const matchQuote = regexQuote.exec(text);
187
+ if (matchQuote) {
188
+ const quote = matchQuote[0].slice(-1);
189
+ const exprStart = `${key}:[\\s]+\\${quote}`;
190
+ const exprEnd = `(\\${quote},?)`;
191
+ const regexKey = new RegExp(`${exprStart}[\\s\\S]*?${exprEnd}`, "gm");
192
+ const matchKey = regexKey.exec(text);
193
+ if (matchKey) {
194
+ const regexStart = new RegExp(`${exprStart}`, "gm");
195
+ const regexEnd = new RegExp(`${exprEnd}`, "gm");
196
+ return matchKey[0]
197
+ .replace(regexStart, "")
198
+ .replace(regexEnd, "")
199
+ .replace("fixable:", "")
200
+ .replace(/\\/gm, "")
201
+ .trim();
202
+ } else {
203
+ return "";
204
+ }
205
+ } else {
206
+ const regexBoolean = new RegExp(`${key}:[\\s]+true[\\s]?,`, "gm");
207
+ const matchBoolean = regexBoolean.exec(text);
208
+ if (matchBoolean) {
209
+ if (matchBoolean[0].includes("true,")) {
210
+ return true;
211
+ } else {
212
+ return false;
213
+ }
214
+ }
215
+ return "";
216
+ }
217
+ },
218
+
219
+ /**
220
+ * Gets value for a given key in allowed keys for input of runRuleTester api
221
+ * @param {string} text test input for ruleTester
222
+ * @param {string} key key to get value for
223
+ * @returns Value for given key
224
+ */
225
+ getKeyFromTest: function (text, key) {
226
+ let result = "";
227
+ if (["root", "rule", "filename", "parser"].includes(key)) {
228
+ const regexTestKey = new RegExp(`${key}:.*$`, "gm");
229
+ const matchTestKey = regexTestKey.exec(text);
230
+ if (matchTestKey) {
231
+ const quote = matchTestKey[0].replace(",", "").slice(-1);
232
+ const regexTestValue = new RegExp(
233
+ `${quote}[\\s\\S]*?(\\${quote},?)`,
234
+ "gm"
235
+ );
236
+ const matchValue = regexTestValue.exec(matchTestKey[0]);
237
+ if (matchValue) {
238
+ const regex = new RegExp(`${quote},`, "gm");
239
+ const regex2 = new RegExp(`${quote}`, "gm");
240
+ result = matchValue[0].replace(regex, "").replace(regex2, "");
241
+ }
242
+ }
243
+ } else if (key === "errors") {
244
+ const regexTestKey = new RegExp(`${key}:.*$(([\\s]+.+)+])?`, "gm");
245
+ const matchTestKey = regexTestKey.exec(text);
246
+ if (matchTestKey) {
247
+ result = matchTestKey[0];
248
+ }
249
+ } else {
250
+ result = `No parameter \\'${key}\\' found in ruleTest`;
251
+ }
252
+ return result;
253
+ },
254
+
255
+ /**
256
+ * Generates overview table of all rules based on rule dictionary.
257
+ * @param ruleDict
258
+ * @param release
259
+ * @param table
260
+ * @returns Markdown table
261
+ */
262
+ genMdRules: function (ruleDict, release, table = true) {
263
+ let version = "latest";
264
+ if (release) {
265
+ version = release;
266
+ }
267
+ let mdRules = `# @sap/eslint-plugin-cds [${version}]\n\n`;
268
+ if (table) {
269
+ mdRules += "| | | | | |\n";
270
+ mdRules += "|:-:|:-:|:-:|:-:|:-|\n";
271
+ /* eslint-disable-next-line no-unused-vars */
272
+ Object.entries(ruleDict).forEach(([, rules]) => {
273
+ rules.forEach(function (rule) {
274
+ if (release) {
275
+ mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | [${rule.name}](Rules-released.md#rule-${rule.name}) | ${rule.details}|\n`;
276
+ } else {
277
+ mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | [${rule.name}](Rules.md#rule-${rule.name}) | ${rule.details}|\n`;
278
+ }
279
+ });
280
+ });
281
+ mdRules += "\n";
282
+ }
283
+ return mdRules;
284
+ },
285
+
286
+ /**
287
+ * Generates markdown documentation files for:
288
+ * - Overview of all rules in form of markdown table (RuleList)
289
+ * - List of all rules details in form of markdown page (Rules)
290
+ * If used internally within the @sap/eslint-plugin-cds, this
291
+ * also generates 'released' files, which only contain information
292
+ * on rules published until the currently released version.
293
+ * @param ruleDict
294
+ * @param docsPath
295
+ * @param release
296
+ */
297
+ genDocFiles: function (ruleDict, docsPath, release) {
298
+ let suffix = "";
299
+ if (release) {
300
+ suffix = "-released";
301
+ }
302
+ const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`);
303
+ const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`);
304
+
305
+ if (!fs.existsSync(ruleDocsPath)) {
306
+ fs.writeFileSync(ruleDocsPath, "", "utf8");
307
+ }
308
+ if (!fs.existsSync(ruleListDocsPath)) {
309
+ fs.writeFileSync(ruleListDocsPath, "", "utf8");
310
+ }
311
+ const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
312
+ const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
313
+
314
+ let mdRules = module.exports.genMdRules(ruleDict, release, false);
315
+ let mdRuleList = module.exports.genMdRules(ruleDict);
316
+ /* eslint-disable-next-line no-unused-vars */
317
+ Object.entries(ruleDict).forEach(([category, rules]) => {
318
+ rules.forEach(function (rule) {
319
+ mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`;
320
+ });
321
+ });
322
+
323
+ if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
324
+ fs.writeFileSync(ruleDocsPath, mdRules, "utf8");
325
+ fs.writeFileSync(ruleListDocsPath, mdRuleList, "utf8");
326
+ }
327
+ },
328
+
329
+ /**
330
+ * Generates custom rules documentation (markdown files)
331
+ * for user according to contents of:
332
+ * - Rule files
333
+ * - Test files (with valid/invalid/fixed examples)
334
+ * @param {string} projectPath
335
+ * @param {string} customRulesDir
336
+ */
337
+ async genDocs(projectPath, customRulesDir) {
338
+ let mdRule, mdRuleSources, mdRuleContents;
339
+ let docsPath, rulePath, testPath, release;
340
+
341
+ if (!projectPath) {
342
+ docsPath = path.join(__dirname, "../../../docs");
343
+ rulePath = path.join(__dirname, "../rules");
344
+ testPath = path.join(__dirname, "../../../test/rules");
345
+ release = JSON.parse(
346
+ fs.readFileSync(path.join(__dirname, "../../../package.json"))
347
+ ).version;
348
+ } else {
349
+ docsPath = path.join(projectPath, `${customRulesDir}/docs`);
350
+ rulePath = path.join(projectPath, `${customRulesDir}/rules`);
351
+ testPath = path.join(projectPath, `${customRulesDir}/tests`);
352
+ }
353
+
354
+ if (!fs.existsSync(docsPath)) {
355
+ await mkdirp(docsPath);
356
+ }
357
+ if (!fs.existsSync(rulePath)) {
358
+ await mkdirp(rulePath);
359
+ }
360
+ if (!fs.existsSync(testPath)) {
361
+ await mkdirp(testPath);
362
+ }
363
+
364
+ const ruleDict = {};
365
+ fs.readdirSync(rulePath).filter(function (file) {
366
+ if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
367
+ const rule = path.basename(file).replace(path.extname(file), "");
368
+ if (rule !== "cds-compile-error") {
369
+ const ruleTestPath = path.join(testPath, rule, "rule.test.js");
370
+
371
+ // Get rule meta information
372
+ const ruleMeta = fs.readFileSync(path.join(rulePath, file), "utf8");
373
+ const details = module.exports.getKeyFromMeta(
374
+ ruleMeta,
375
+ "description"
376
+ );
377
+ const category = module.exports.getKeyFromMeta(ruleMeta, "category");
378
+ const fixable = module.exports.getKeyFromMeta(ruleMeta, "fixable");
379
+ const suggestions = module.exports.getKeyFromMeta(
380
+ ruleMeta,
381
+ "hasSuggestions"
382
+ );
383
+
384
+ let isFixable = "";
385
+ if (["code", "whitespace"].includes(fixable)) {
386
+ isFixable = "🔧";
387
+ }
388
+
389
+ let isRecommended = "";
390
+ if (Object.keys(recommended).includes(`@sap/cds/${rule}`)) {
391
+ isRecommended = "✔️";
392
+ }
393
+
394
+ let hasSuggestions = "";
395
+ if (suggestions === true) {
396
+ hasSuggestions = "💡";
397
+ }
398
+
399
+ const version = module.exports.getKeyFromMeta(ruleMeta, "version");
400
+
401
+ // Get rule valid/invalid tests
402
+ mdRule = "";
403
+ if (fs.existsSync(ruleTestPath)) {
404
+ const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
405
+ const filename = module.exports.getKeyFromTest(
406
+ ruleTest,
407
+ "filename"
408
+ );
409
+ let errorsString = module.exports.getKeyFromTest(
410
+ ruleTest,
411
+ "errors"
412
+ );
413
+ const re = /(\S+):/gm;
414
+ errorsString = errorsString
415
+ .replace(re, `"$&`)
416
+ .replace(/:/gm, '":')
417
+ .replace(/`/gm, '"');
418
+ const errors = JSONC.parse(`{${errorsString}}`).errors;
419
+ const valid = fs.readFileSync(
420
+ path.join(testPath, rule, "valid", filename),
421
+ "utf8"
422
+ );
423
+ let invalid = fs.readFileSync(
424
+ path.join(testPath, rule, "invalid", filename),
425
+ "utf8"
426
+ );
427
+ const insertAt = (str, sub, pos) =>
428
+ `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
429
+ errors.forEach((err) => {
430
+ if (err.messageId) {
431
+ err.message = err.messageId;
432
+ }
433
+ const msg = err.message.replace(/"/gm, "`");
434
+ if (err.line) {
435
+ const code = invalid.split("\n");
436
+ code[err.line - 1] = insertAt(
437
+ code[err.line - 1],
438
+ "</span>",
439
+ err.endColumn - 1
440
+ );
441
+ code[err.line - 1] = insertAt(
442
+ code[err.line - 1],
443
+ `<span style="text-decoration-line:underline; text-decoration-style:wavy; text-decoration-color:red;" title="${msg}">`,
444
+ err.column - 1
445
+ );
446
+ invalid = code.join("\n");
447
+ }
448
+ });
449
+
450
+ mdRule +=
451
+ `<span>✔️&nbsp;&nbsp; Example of ` +
452
+ `<span style="color:green">correct</span> ` +
453
+ `code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
454
+ mdRule +=
455
+ `<span>❌&nbsp;&nbsp; Example of ` +
456
+ `<span style="color:red">incorrect</span> ` +
457
+ `code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
458
+ }
459
+
460
+ mdRuleContents = `## ${rule}\n`;
461
+ mdRuleContents += `<span class='label shifted'>${category}</span>\n\n`;
462
+ mdRuleContents += `### Rule Details\n${details}\n\n`;
463
+ if (mdRule) {
464
+ mdRuleContents += `### Examples\n${mdRule}\n\n`;
465
+ }
466
+ mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
467
+ mdRuleSources = `### Resources\n[Rule & Documentation source](${path
468
+ .relative(docsPath, path.join(rulePath, `${rule}.js`))
469
+ .replace(/\\/g, "/")})\n\n`;
470
+
471
+ if (Object.keys(ruleDict).includes(category)) {
472
+ ruleDict[category].push({
473
+ name: rule,
474
+ details,
475
+ recommended: isRecommended,
476
+ fixable: isFixable,
477
+ hasSuggestions,
478
+ version: version,
479
+ contents: mdRuleContents,
480
+ sources: mdRuleSources,
481
+ });
482
+ } else {
483
+ ruleDict[category] = [
484
+ {
485
+ name: rule,
486
+ details,
487
+ recommended: isRecommended,
488
+ fixable: isFixable,
489
+ hasSuggestions,
490
+ version: version,
491
+ contents: mdRuleContents,
492
+ sources: mdRuleSources,
493
+ },
494
+ ];
495
+ }
496
+ }
497
+ }
498
+ });
499
+
500
+ module.exports.genDocFiles(ruleDict, docsPath);
501
+ if (release) {
502
+ module.exports.genDocFiles(ruleDict, docsPath, release);
503
+ }
504
+ },
505
+
506
+ /**
507
+ * Gets all relevant rules information (dictionary of rules contents, lists
508
+ * of 'env' and 'model' rules) for the rules directory provided
509
+ * - Default categories is 'model'
510
+ * @param {string} dirname
511
+ * @returns rules information
512
+ */
513
+ getRules(dirname) {
514
+ const rules = {};
515
+ const listEnvRules = [];
516
+ const listModelRules = [];
517
+ fs.readdirSync(dirname).forEach((file) => {
518
+ if (path.extname(file) === ".js" && file !== "index.js") {
519
+ const rulename = file.replace(".js", "");
520
+ const ruleID = `${rulename}`;
521
+ rules[ruleID] = require(path.join(dirname, file));
522
+ const category =
523
+ rules[ruleID].meta.docs.category || categories["model"];
524
+ if (
525
+ !listEnvRules.includes(rulename) &&
526
+ category === categories["env"]
527
+ ) {
528
+ listEnvRules.push(rulename);
529
+ }
530
+ if (
531
+ !listModelRules.includes(rulename) &&
532
+ category === categories["model"]
533
+ ) {
534
+ listModelRules.push(rulename);
535
+ }
536
+ }
537
+ });
538
+ const listRules = listEnvRules.concat(listModelRules);
539
+ return { rules, listRules, listEnvRules, listModelRules };
540
+ },
541
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @typedef { import("eslint").Rule.RuleContext.options } ContextOptions
3
+ */
4
+
5
+ module.exports = {
6
+ /**
7
+ * Checks whether the rule name is valid, as it must be
8
+ * contained in either the plugins rule set or the user's
9
+ * custom rules provided at runtime
10
+ * @param context
11
+ * @param pluginRules
12
+ * @param customRules
13
+ * @returns
14
+ */
15
+ isValidRule: function (context, rules) {
16
+ if (rules.includes(context.ruleID.replace("@sap/cds/", ""))) {
17
+ return true;
18
+ }
19
+ return false;
20
+ },
21
+
22
+ /**
23
+ * Checks whether the compiled cds model is eligible
24
+ * for performing the plugin's rule checks
25
+ * @param cds cds object
26
+ * @param ruleID rule name
27
+ * @returns
28
+ */
29
+ isValidModel: function (context) {
30
+ const { cds, ruleID } = context;
31
+ if (
32
+ cds &&
33
+ cds.model &&
34
+ !cds.model.err &&
35
+ !(
36
+ ruleID === "@sap/cds/cds-compile-error" ||
37
+ ruleID === "cds-compile-error"
38
+ )
39
+ ) {
40
+ return true;
41
+ }
42
+ return false;
43
+ },
44
+
45
+ /**
46
+ * Checks whether the cds environment is passed
47
+ * @param {ContextOptions} options
48
+ * @returns
49
+ */
50
+ isValidEnv: function (options) {
51
+ if (options[0] && options[0].environment) {
52
+ return true;
53
+ }
54
+ return false;
55
+ },
56
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/eslint-plugin-cds",
3
- "version": "2.0.4",
3
+ "version": "2.2.1",
4
4
  "description": "ESLint plugin including recommended SAP Cloud Application Programming model and environment rules",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -14,7 +14,7 @@
14
14
  "license": "See LICENSE file",
15
15
  "main": "lib/impl/index.js",
16
16
  "files": [
17
- "lib/**/*.js",
17
+ "lib/",
18
18
  "CHANGELOG.md",
19
19
  "LICENSE",
20
20
  "README.md"
@@ -24,6 +24,6 @@
24
24
  "semver": "^7.3.4"
25
25
  },
26
26
  "peerDependencies": {
27
- "eslint": ">=6"
27
+ "eslint": ">=7"
28
28
  }
29
29
  }