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