@sap/eslint-plugin-cds 2.1.1 → 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.
@@ -1,25 +1,30 @@
1
+ /**
2
+ * @typedef { import('eslint').Linter.ConfigOverride.files } ConfigOverrideFiles
3
+ */
4
+
1
5
  const fs = require("fs");
2
6
  const path = require("path");
3
7
  const { mkdirp } = require("@sap/cds/lib/utils");
4
8
  const { getLastLine } = require("./model");
5
9
 
6
10
  const JSONC = require("./jsonc");
7
- const CONSTANTS = require("../constants");
8
- const REGEX_COMMENT_START = "(/\\*|(.+)?//)\\s+eslint-";
11
+ const { categories, files, recommended } = require("../constants");
12
+ const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
9
13
  const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
10
14
 
11
15
  module.exports = {
12
16
  /**
13
17
  * Returns an array of allowed file extensions
14
18
  * the plugin can parse of the form "*.ext"
15
- * @returns Array of file extensions
19
+ * @returns {ConfigOverrideFiles} Array of file extensions
16
20
  */
17
21
  getFileExtensions: function () {
18
- return CONSTANTS.files;
22
+ return files;
19
23
  },
20
24
 
21
25
  /**
22
- * Turns rules on/off for given line according to eslint-disable comments:
26
+ * Turns rules "on" or "off" for given line according to eslint-disable
27
+ * comments:
23
28
  * 1. Reads code string and extracts a list of comments (in order)
24
29
  * 2. Initiates rulesDisabled array with all rules "on" by default
25
30
  * 3. Switches rules "off" (or "on" again) based on disable comment
@@ -28,92 +33,152 @@ module.exports = {
28
33
  * @param line current code line to analyze
29
34
  * @returns rules dictionary with rules being either 'on' and 'off'
30
35
  */
31
- getDisabledFromComments: function (rules, code, sourcecode, line) {
36
+ getDisabled: function (
37
+ rules,
38
+ listEnvRules,
39
+ listModelRules,
40
+ code,
41
+ sourcecode,
42
+ line
43
+ ) {
32
44
  const listDisabled = [];
33
45
  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}`);
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];
56
58
  }
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
- };
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());
72
70
  } else {
73
- comment = {
74
- lineComment: loc.line,
75
- lineDisabled: loc.line,
76
- rules: disableRules,
77
- type: keyword,
78
- };
71
+ disableRules = listEnvRules
72
+ .concat(listModelRules)
73
+ .map((rule) => `@sap/cds/${rule}`);
79
74
  }
80
- if (!disableType.includes("-line")) {
81
- comment.lineDisabled = "EOF";
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
+ }
82
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);
83
116
  }
84
- listDisabled.push(comment);
85
- });
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
+ }
86
127
  }
87
- });
88
- for (let i = 0; i <= listDisabled.length - 1; i++) {
89
- if (listDisabled[i].lineComment > line) {
90
- break;
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;
91
164
  }
92
165
  if (
93
- listDisabled[i].lineDisabled === "EOF" ||
94
- listDisabled[i].lineDisabled === line
166
+ line &&
167
+ ruleID in rulesDisabled &&
168
+ rulesDisabled[ruleID] === "off"
95
169
  ) {
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
- }
170
+ isDisabled = true;
108
171
  }
109
172
  }
110
173
  }
111
- return rulesDisabled;
174
+ return isDisabled;
112
175
  },
176
+
113
177
  /**
114
- * Gets value for a given key in allowed keys of ESLint's meta data in createRule api
115
- * @param {*} text
116
- * @param {*} key
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
117
182
  * @returns Value for given key
118
183
  */
119
184
  getKeyFromMeta: function (text, key) {
@@ -122,7 +187,7 @@ module.exports = {
122
187
  if (matchQuote) {
123
188
  const quote = matchQuote[0].slice(-1);
124
189
  const exprStart = `${key}:[\\s]+\\${quote}`;
125
- const exprEnd = `(\\${quote},,?)`;
190
+ const exprEnd = `(\\${quote},?)`;
126
191
  const regexKey = new RegExp(`${exprStart}[\\s\\S]*?${exprEnd}`, "gm");
127
192
  const matchKey = regexKey.exec(text);
128
193
  if (matchKey) {
@@ -138,14 +203,23 @@ module.exports = {
138
203
  return "";
139
204
  }
140
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
+ }
141
215
  return "";
142
216
  }
143
217
  },
144
218
 
145
219
  /**
146
220
  * Gets value for a given key in allowed keys for input of runRuleTester api
147
- * @param {*} text
148
- * @param {*} key
221
+ * @param {string} text test input for ruleTester
222
+ * @param {string} key key to get value for
149
223
  * @returns Value for given key
150
224
  */
151
225
  getKeyFromTest: function (text, key) {
@@ -180,9 +254,9 @@ module.exports = {
180
254
 
181
255
  /**
182
256
  * Generates overview table of all rules based on rule dictionary.
183
- * @param {*} ruleDict
184
- * @param {*} release
185
- * @param {*} table
257
+ * @param ruleDict
258
+ * @param release
259
+ * @param table
186
260
  * @returns Markdown table
187
261
  */
188
262
  genMdRules: function (ruleDict, release, table = true) {
@@ -192,14 +266,19 @@ module.exports = {
192
266
  }
193
267
  let mdRules = `# @sap/eslint-plugin-cds [${version}]\n\n`;
194
268
  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]) => {
269
+ mdRules += "| | | | | |\n";
270
+ mdRules += "|:-:|:-:|:-:|:-:|:-|\n";
271
+ /* eslint-disable-next-line no-unused-vars */
272
+ Object.entries(ruleDict).forEach(([, rules]) => {
198
273
  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>`;
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
+ }
200
279
  });
201
280
  });
202
- mdRules += "</table>\n\n<br>\n\n";
281
+ mdRules += "\n";
203
282
  }
204
283
  return mdRules;
205
284
  },
@@ -211,9 +290,9 @@ module.exports = {
211
290
  * If used internally within the @sap/eslint-plugin-cds, this
212
291
  * also generates 'released' files, which only contain information
213
292
  * on rules published until the currently released version.
214
- * @param {*} ruleDict
215
- * @param {*} docsPath
216
- * @param {*} release
293
+ * @param ruleDict
294
+ * @param docsPath
295
+ * @param release
217
296
  */
218
297
  genDocFiles: function (ruleDict, docsPath, release) {
219
298
  let suffix = "";
@@ -252,7 +331,8 @@ module.exports = {
252
331
  * for user according to contents of:
253
332
  * - Rule files
254
333
  * - Test files (with valid/invalid/fixed examples)
255
- * @param {*} projectPath
334
+ * @param {string} projectPath
335
+ * @param {string} customRulesDir
256
336
  */
257
337
  async genDocs(projectPath, customRulesDir) {
258
338
  let mdRule, mdRuleSources, mdRuleContents;
@@ -296,6 +376,10 @@ module.exports = {
296
376
  );
297
377
  const category = module.exports.getKeyFromMeta(ruleMeta, "category");
298
378
  const fixable = module.exports.getKeyFromMeta(ruleMeta, "fixable");
379
+ const suggestions = module.exports.getKeyFromMeta(
380
+ ruleMeta,
381
+ "hasSuggestions"
382
+ );
299
383
 
300
384
  let isFixable = "";
301
385
  if (["code", "whitespace"].includes(fixable)) {
@@ -303,10 +387,15 @@ module.exports = {
303
387
  }
304
388
 
305
389
  let isRecommended = "";
306
- if (Object.keys(CONSTANTS.recommended).includes(`@sap/cds/${rule}`)) {
390
+ if (Object.keys(recommended).includes(`@sap/cds/${rule}`)) {
307
391
  isRecommended = "✔️";
308
392
  }
309
393
 
394
+ let hasSuggestions = "";
395
+ if (suggestions === true) {
396
+ hasSuggestions = "💡";
397
+ }
398
+
310
399
  const version = module.exports.getKeyFromMeta(ruleMeta, "version");
311
400
 
312
401
  // Get rule valid/invalid tests
@@ -338,6 +427,9 @@ module.exports = {
338
427
  const insertAt = (str, sub, pos) =>
339
428
  `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
340
429
  errors.forEach((err) => {
430
+ if (err.messageId) {
431
+ err.message = err.messageId;
432
+ }
341
433
  const msg = err.message.replace(/"/gm, "`");
342
434
  if (err.line) {
343
435
  const code = invalid.split("\n");
@@ -365,24 +457,24 @@ module.exports = {
365
457
  `code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
366
458
  }
367
459
 
368
- mdRuleContents = `## Rule: ${rule}\n`;
460
+ mdRuleContents = `## ${rule}\n`;
369
461
  mdRuleContents += `<span class='label shifted'>${category}</span>\n\n`;
370
462
  mdRuleContents += `### Rule Details\n${details}\n\n`;
371
463
  if (mdRule) {
372
464
  mdRuleContents += `### Examples\n${mdRule}\n\n`;
373
465
  }
374
466
  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`;
467
+ mdRuleSources = `### Resources\n[Rule & Documentation source](${path
468
+ .relative(docsPath, path.join(rulePath, `${rule}.js`))
469
+ .replace(/\\/g, "/")})\n\n`;
379
470
 
380
471
  if (Object.keys(ruleDict).includes(category)) {
381
472
  ruleDict[category].push({
382
473
  name: rule,
383
474
  details,
384
475
  recommended: isRecommended,
385
- fix: isFixable,
476
+ fixable: isFixable,
477
+ hasSuggestions,
386
478
  version: version,
387
479
  contents: mdRuleContents,
388
480
  sources: mdRuleSources,
@@ -393,7 +485,8 @@ module.exports = {
393
485
  name: rule,
394
486
  details,
395
487
  recommended: isRecommended,
396
- fix: isFixable,
488
+ fixable: isFixable,
489
+ hasSuggestions,
397
490
  version: version,
398
491
  contents: mdRuleContents,
399
492
  sources: mdRuleSources,
@@ -410,48 +503,39 @@ module.exports = {
410
503
  }
411
504
  },
412
505
 
413
- getRules: function (dirname) {
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) {
414
514
  const rules = {};
515
+ const listEnvRules = [];
516
+ const listModelRules = [];
415
517
  fs.readdirSync(dirname).forEach((file) => {
416
518
  if (path.extname(file) === ".js" && file !== "index.js") {
417
- rules[file.replace(".js", "")] = require(path.join(dirname, file));
418
- }
419
- });
420
- return rules;
421
- },
422
-
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", ""));
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);
437
529
  }
438
- }
439
- });
440
- return envRules;
441
- },
442
-
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", ""));
530
+ if (
531
+ !listModelRules.includes(rulename) &&
532
+ category === categories["model"]
533
+ ) {
534
+ listModelRules.push(rulename);
452
535
  }
453
536
  }
454
537
  });
455
- return modelRules;
538
+ const listRules = listEnvRules.concat(listModelRules);
539
+ return { rules, listRules, listEnvRules, listModelRules };
456
540
  },
457
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.1.1",
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": [
@@ -1,5 +0,0 @@
1
- const { getRules } = require("../utils/rules");
2
-
3
- const rules = getRules(__dirname);
4
-
5
- module.exports = { rules };
@@ -1,10 +0,0 @@
1
- const path = require("path");
2
- const { runRuleTester } = require("../../../lib/api");
3
-
4
- runRuleTester({
5
- root: path.resolve(__dirname),
6
- rule: require(`../../../lib/rules/${path.basename(__dirname)}`),
7
- filename: "{{filename}}",
8
- parser: path.resolve(path.join(__dirname, '../../../lib/impl/parser')),
9
- errors: "{{errors}}",
10
- });