@sap/eslint-plugin-cds 2.2.2 → 2.3.0
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.
- package/CHANGELOG.md +4 -54
- package/lib/impl/constants.js +1 -11
- package/lib/impl/index.js +6 -1
- package/lib/impl/ruleFactory.js +44 -14
- package/lib/impl/rules/assoc2many-ambiguous-key.js +4 -2
- package/lib/impl/rules/cds-compile-error.js +21 -22
- package/lib/impl/rules/latest-cds-version.js +6 -1
- package/lib/impl/rules/min-node-version.js +3 -1
- package/lib/impl/rules/no-db-keywords.js +3 -0
- package/lib/impl/rules/no-join-on-draft-enabled-entities.js +3 -1
- package/lib/impl/rules/require-2many-oncond.js +3 -1
- package/lib/impl/rules/sql-cast-suggestion.js +3 -1
- package/lib/impl/rules/start-elements-lowercase.js +3 -2
- package/lib/impl/rules/start-entities-uppercase.js +2 -2
- package/lib/impl/rules/valid-csv-header.js +92 -0
- package/lib/impl/utils/fuzzySearch.js +87 -0
- package/lib/impl/utils/model.js +52 -26
- package/lib/impl/utils/rules.js +269 -141
- package/lib/impl/utils/validate.js +6 -3
- package/package.json +2 -2
package/lib/impl/utils/model.js
CHANGED
|
@@ -101,20 +101,22 @@ module.exports = {
|
|
|
101
101
|
module.exports.Cache.set("filepath", context.filePath);
|
|
102
102
|
|
|
103
103
|
// Get CDS reflected model
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
process.env["LINT_FLAVOR"] !== "parsed" &&
|
|
107
|
+
(module.exports.isNewFile(context.filePath) ||
|
|
108
108
|
module.exports.isNewConfigPath(context.configPath))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
) {
|
|
110
|
+
module.exports.initModel(context.configPath, context.filePath);
|
|
111
|
+
}
|
|
112
|
+
// Trigger model updates for:
|
|
113
|
+
// - Changed 'model' files
|
|
114
|
+
// - Any 'outsider' files
|
|
115
|
+
if (
|
|
116
|
+
isValidFile(context.filePath, "model") &&
|
|
117
|
+
module.exports.hasFileChanged(context)
|
|
118
|
+
) {
|
|
119
|
+
module.exports.updateModel(context);
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
// Get cds environment (for internal ruleTester)
|
|
@@ -146,6 +148,7 @@ module.exports = {
|
|
|
146
148
|
isNewConfigPath: function (configPath) {
|
|
147
149
|
let update = false;
|
|
148
150
|
if (
|
|
151
|
+
!module.exports.Cache.has("pluginpath") ||
|
|
149
152
|
!module.exports.Cache.has("configpath") ||
|
|
150
153
|
configPath !== module.exports.Cache.get("configpath")
|
|
151
154
|
) {
|
|
@@ -426,6 +429,30 @@ module.exports = {
|
|
|
426
429
|
}
|
|
427
430
|
},
|
|
428
431
|
|
|
432
|
+
initModelRuleTester: function (filePath) {
|
|
433
|
+
const configPath = path.dirname(filePath);
|
|
434
|
+
module.exports.Cache.set("configpath", configPath);
|
|
435
|
+
let files = fs.readdirSync(configPath);
|
|
436
|
+
const modelfiles = [];
|
|
437
|
+
files.forEach((file) => {
|
|
438
|
+
const filePath = path.join(configPath, file);
|
|
439
|
+
if (isValidFile(filePath, "model")) {
|
|
440
|
+
modelfiles.push(filePath);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
module.exports.Cache.set(`modelfiles:${configPath}`, files);
|
|
444
|
+
const dictFiles = module.exports.getDictFiles(configPath, modelfiles);
|
|
445
|
+
module.exports.Cache.set(`dictfiles:${configPath}`, dictFiles);
|
|
446
|
+
const compiledModel = module.exports.compileModelFromDict(dictFiles);
|
|
447
|
+
let reflectedModel;
|
|
448
|
+
if (compiledModel) {
|
|
449
|
+
reflectedModel = cds.linked(compiledModel);
|
|
450
|
+
}
|
|
451
|
+
if (reflectedModel) {
|
|
452
|
+
module.exports.Cache.set(`model:${filePath}`, reflectedModel);
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
|
|
429
456
|
/**
|
|
430
457
|
* Creates or updates a dictionary of files/file contents for a given
|
|
431
458
|
* project path.
|
|
@@ -433,7 +460,7 @@ module.exports = {
|
|
|
433
460
|
* @param files
|
|
434
461
|
* @returns dictFiles
|
|
435
462
|
*/
|
|
436
|
-
getDictFiles: function (configPath, files) {
|
|
463
|
+
getDictFiles: function (configPath, files=[]) {
|
|
437
464
|
let dictFiles = {};
|
|
438
465
|
if (module.exports.Cache.has(`dictfiles:${configPath}`)) {
|
|
439
466
|
dictFiles = module.exports.Cache.get(`dictfiles:${configPath}`);
|
|
@@ -455,11 +482,10 @@ module.exports = {
|
|
|
455
482
|
* @returns boolean
|
|
456
483
|
*/
|
|
457
484
|
hasFileChanged: function (context) {
|
|
458
|
-
let dictFiles = {};
|
|
459
485
|
const files = module.exports.Cache.get(`modelfiles:${context.configPath}`);
|
|
486
|
+
const dictFiles = module.exports.getDictFiles(context.configPath, files);
|
|
460
487
|
// If incoming file is a 'model' file
|
|
461
488
|
if (module.exports.isFileInModel(context, files)) {
|
|
462
|
-
dictFiles = module.exports.getDictFiles(context.configPath, files);
|
|
463
489
|
// Only update on detected changes
|
|
464
490
|
if (dictFiles[context.filePath] !== context.code) {
|
|
465
491
|
dictFiles[context.filePath] = context.code;
|
|
@@ -467,7 +493,9 @@ module.exports = {
|
|
|
467
493
|
return true;
|
|
468
494
|
}
|
|
469
495
|
} else {
|
|
470
|
-
|
|
496
|
+
if (dictFiles[context.filePath] !== context.code) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
471
499
|
}
|
|
472
500
|
return false;
|
|
473
501
|
},
|
|
@@ -478,8 +506,8 @@ module.exports = {
|
|
|
478
506
|
* @param files
|
|
479
507
|
* @returns boolean
|
|
480
508
|
*/
|
|
481
|
-
isFileInModel(
|
|
482
|
-
if (files && files.length > 0 && files.includes(
|
|
509
|
+
isFileInModel(filePath, files) {
|
|
510
|
+
if (files && files.length > 0 && files.includes(filePath)) {
|
|
483
511
|
return true;
|
|
484
512
|
}
|
|
485
513
|
return false;
|
|
@@ -498,7 +526,10 @@ module.exports = {
|
|
|
498
526
|
files = [];
|
|
499
527
|
}
|
|
500
528
|
// If incoming file is a 'model' file
|
|
501
|
-
if (
|
|
529
|
+
if (
|
|
530
|
+
!process.env["LINT_FLAVOR"] === "parsed" ||
|
|
531
|
+
module.exports.isFileInModel(context, files)
|
|
532
|
+
) {
|
|
502
533
|
const dictFiles = module.exports.Cache.get(
|
|
503
534
|
`dictfiles:${context.configPath}`
|
|
504
535
|
);
|
|
@@ -514,15 +545,10 @@ module.exports = {
|
|
|
514
545
|
const dictFiles = {};
|
|
515
546
|
dictFiles[context.filePath] = context.code;
|
|
516
547
|
let flavor = "parsed";
|
|
517
|
-
// Fully resolve model for ESLint's ruleTester
|
|
518
|
-
if (process.env["RULE_TESTER"]) {
|
|
519
|
-
flavor = "inferred"
|
|
520
|
-
}
|
|
521
548
|
reflectedModel = module.exports.compileModelFromDict(dictFiles, {
|
|
522
|
-
flavor
|
|
549
|
+
flavor,
|
|
523
550
|
});
|
|
524
551
|
module.exports.Cache.set(`model:${context.filePath}`, reflectedModel);
|
|
525
552
|
}
|
|
526
553
|
},
|
|
527
|
-
|
|
528
554
|
};
|
package/lib/impl/utils/rules.js
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const cp = require("child_process");
|
|
8
|
+
const semver = require("semver");
|
|
6
9
|
const path = require("path");
|
|
7
10
|
const { mkdirp } = require("@sap/cds/lib/utils");
|
|
8
11
|
const { Cache, getLastLine } = require("./model");
|
|
9
12
|
|
|
10
13
|
const JSONC = require("./jsonc");
|
|
11
|
-
const { categories
|
|
14
|
+
const { categories } = require("../constants");
|
|
15
|
+
const IS_WIN = os.platform() === "win32";
|
|
12
16
|
const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
|
|
13
17
|
const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
|
|
14
18
|
|
|
@@ -195,6 +199,27 @@ module.exports = {
|
|
|
195
199
|
}
|
|
196
200
|
},
|
|
197
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
|
+
|
|
198
223
|
/**
|
|
199
224
|
* Gets value for a given key in allowed keys for input of runRuleTester api
|
|
200
225
|
* @param {string} text test input for ruleTester
|
|
@@ -225,6 +250,12 @@ module.exports = {
|
|
|
225
250
|
if (matchTestKey) {
|
|
226
251
|
result = matchTestKey[0];
|
|
227
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
|
+
}
|
|
228
259
|
} else {
|
|
229
260
|
result = `No parameter \\'${key}\\' found in ruleTest`;
|
|
230
261
|
}
|
|
@@ -239,26 +270,27 @@ module.exports = {
|
|
|
239
270
|
* @returns Markdown table
|
|
240
271
|
*/
|
|
241
272
|
genMdRules: function (ruleDict, release, table = true) {
|
|
242
|
-
let
|
|
243
|
-
if (release) {
|
|
244
|
-
version = release;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let mdRules = `# @sap/eslint-plugin-cds [${version}]\n\n`;
|
|
273
|
+
let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
|
|
248
274
|
if (table) {
|
|
249
275
|
mdRules += `Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n`;
|
|
250
276
|
mdRules += `✔️ if the plugin's "recommended" configuration enables the rule\n\n`;
|
|
251
277
|
mdRules += `🔧 if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`;
|
|
252
278
|
mdRules += `💡 if problems reported by the rule are manually fixable (editor)\n\n`;
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
287
|
/* eslint-disable-next-line no-unused-vars */
|
|
256
288
|
Object.entries(ruleDict).forEach(([, rules]) => {
|
|
257
289
|
rules.forEach(function (rule) {
|
|
258
290
|
if (release) {
|
|
259
|
-
mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`;
|
|
291
|
+
mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | | | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`;
|
|
260
292
|
} else {
|
|
261
|
-
mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
|
|
293
|
+
mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | ${rule.construction} | | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
|
|
262
294
|
}
|
|
263
295
|
});
|
|
264
296
|
});
|
|
@@ -278,7 +310,7 @@ module.exports = {
|
|
|
278
310
|
* @param docsPath
|
|
279
311
|
* @param release
|
|
280
312
|
*/
|
|
281
|
-
genDocFiles: function (ruleDict, docsPath, release) {
|
|
313
|
+
genDocFiles: function (ruleDict, docsPath, release=false) {
|
|
282
314
|
let suffix = "";
|
|
283
315
|
if (release) {
|
|
284
316
|
suffix = "-released";
|
|
@@ -295,8 +327,11 @@ module.exports = {
|
|
|
295
327
|
const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
|
|
296
328
|
const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
|
|
297
329
|
|
|
330
|
+
// Get rules table
|
|
331
|
+
let mdRuleList = module.exports.genMdRules(ruleDict, release, true);
|
|
332
|
+
|
|
333
|
+
// Get rule details
|
|
298
334
|
let mdRules = module.exports.genMdRules(ruleDict, release, false);
|
|
299
|
-
let mdRuleList = module.exports.genMdRules(ruleDict);
|
|
300
335
|
/* eslint-disable-next-line no-unused-vars */
|
|
301
336
|
Object.entries(ruleDict).forEach(([category, rules]) => {
|
|
302
337
|
rules.forEach(function (rule) {
|
|
@@ -318,8 +353,7 @@ module.exports = {
|
|
|
318
353
|
* @param {string} projectPath
|
|
319
354
|
* @param {string} customRulesDir
|
|
320
355
|
*/
|
|
321
|
-
async genDocs(projectPath, customRulesDir) {
|
|
322
|
-
let mdRule, mdRuleSources, mdRuleContents;
|
|
356
|
+
async genDocs(projectPath, customRulesDir, registry) {
|
|
323
357
|
let docsPath, rulePath, testPath, release;
|
|
324
358
|
|
|
325
359
|
if (!projectPath) {
|
|
@@ -333,19 +367,40 @@ module.exports = {
|
|
|
333
367
|
docsPath = path.join(projectPath, `${customRulesDir}/docs`);
|
|
334
368
|
rulePath = path.join(projectPath, `${customRulesDir}/rules`);
|
|
335
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
|
+
}
|
|
336
379
|
}
|
|
337
380
|
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
381
|
+
if (registry) {
|
|
382
|
+
// Get rules (internal on artifactory)
|
|
383
|
+
const versionInternal = module.exports.getPackageVersion(registry);
|
|
384
|
+
if (versionInternal) {
|
|
385
|
+
const ruleDictInternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionInternal);
|
|
386
|
+
module.exports.genDocFiles(ruleDictInternal, docsPath);
|
|
387
|
+
}
|
|
388
|
+
// Get rules released (external on npm)
|
|
389
|
+
const versionExternal = module.exports.getPackageVersion("https://registry.npmjs.org");
|
|
390
|
+
if (versionExternal) {
|
|
391
|
+
const ruleDictExternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionExternal, release);
|
|
392
|
+
module.exports.genDocFiles(ruleDictExternal, docsPath, release);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
// Get "custom" rules
|
|
396
|
+
const ruleDict = module.exports.getRuleDict(docsPath, rulePath, testPath);
|
|
397
|
+
module.exports.genDocFiles(ruleDict, docsPath);
|
|
346
398
|
}
|
|
399
|
+
},
|
|
347
400
|
|
|
348
|
-
|
|
401
|
+
getRuleDict: function (docsPath, rulePath, testPath, versionRequired='0.0.0', release=false) {
|
|
402
|
+
let mdRule, mdRuleSources, mdRuleContents;
|
|
403
|
+
let ruleDict = {};
|
|
349
404
|
fs.readdirSync(rulePath).filter(function (file) {
|
|
350
405
|
if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
|
|
351
406
|
const rule = path.basename(file).replace(path.extname(file), "");
|
|
@@ -353,138 +408,181 @@ module.exports = {
|
|
|
353
408
|
const ruleTestPath = path.join(testPath, rule, "rule.test.js");
|
|
354
409
|
|
|
355
410
|
// Get rule meta information
|
|
356
|
-
const ruleMeta =
|
|
357
|
-
const
|
|
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
|
-
);
|
|
411
|
+
const ruleMeta = require(path.join(rulePath, file)).meta;
|
|
412
|
+
const version = ruleMeta.docs.version;
|
|
367
413
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
414
|
+
if ((release && semver.satisfies(version, `<=${versionRequired}`))
|
|
415
|
+
|| (!release)) {
|
|
416
|
+
const details = ruleMeta.docs.description;
|
|
417
|
+
const category = ruleMeta.docs.category;
|
|
418
|
+
const fixable = ruleMeta.fixable;
|
|
419
|
+
const messages = ruleMeta.messages;
|
|
420
|
+
const recommended = ruleMeta.docs.recommended;
|
|
421
|
+
const suggestions = ruleMeta.hasSuggestions;
|
|
372
422
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let hasSuggestions = "";
|
|
379
|
-
if (suggestions === true) {
|
|
380
|
-
hasSuggestions = "💡";
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const version = module.exports.getKeyFromMeta(ruleMeta, "version");
|
|
423
|
+
let underConstruction = "";
|
|
424
|
+
if (!release && semver.satisfies(version, `>${versionRequired}`)) {
|
|
425
|
+
underConstruction = "🚧";
|
|
426
|
+
}
|
|
384
427
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
});
|
|
428
|
+
let isFixable = "";
|
|
429
|
+
if (["code", "whitespace"].includes(fixable)) {
|
|
430
|
+
isFixable = "🔧";
|
|
431
|
+
}
|
|
433
432
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
mdRule +=
|
|
439
|
-
`<span>❌ 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
|
-
}
|
|
433
|
+
let isRecommended = "";
|
|
434
|
+
if (recommended === true) {
|
|
435
|
+
isRecommended = "✔️";
|
|
436
|
+
}
|
|
443
437
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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`;
|
|
438
|
+
let hasSuggestions = "";
|
|
439
|
+
if (suggestions === true) {
|
|
440
|
+
hasSuggestions = "💡";
|
|
441
|
+
}
|
|
454
442
|
|
|
455
|
-
|
|
456
|
-
ruleDict[category].push({
|
|
443
|
+
const ruleDictEntry = {
|
|
457
444
|
name: rule,
|
|
458
445
|
details,
|
|
459
446
|
recommended: isRecommended,
|
|
460
447
|
fixable: isFixable,
|
|
461
448
|
hasSuggestions,
|
|
449
|
+
construction: underConstruction,
|
|
450
|
+
messages,
|
|
462
451
|
version: version,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
452
|
+
};
|
|
453
|
+
mdRule = module.exports.getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
|
|
454
|
+
mdRuleContents = "";
|
|
455
|
+
if (!release && underConstruction) {
|
|
456
|
+
mdRuleContents += `## ${rule}\n<span class='shifted'>${underConstruction} <span class='label'>${category}</span></span>\n\n`;
|
|
457
|
+
} else {
|
|
458
|
+
mdRuleContents += `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`;
|
|
459
|
+
}
|
|
460
|
+
mdRuleContents += `### Rule Details\n${details}\n\n`;
|
|
461
|
+
if (mdRule) {
|
|
462
|
+
mdRuleContents += `### Examples\n${mdRule}\n\n`;
|
|
463
|
+
}
|
|
464
|
+
mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
|
|
465
|
+
mdRuleSources = `### Resources\n[Rule & Documentation source](${path
|
|
466
|
+
.relative(docsPath, path.join(rulePath, `${rule}.js`))
|
|
467
|
+
.replace(/\\/g, "/")})\n\n`;
|
|
468
|
+
|
|
469
|
+
ruleDictEntry.contents = mdRuleContents;
|
|
470
|
+
ruleDictEntry.sources = mdRuleSources;
|
|
471
|
+
if (Object.keys(ruleDict).includes(category)) {
|
|
472
|
+
ruleDict[category].push(ruleDictEntry);
|
|
473
|
+
} else {
|
|
474
|
+
ruleDict[category] = [
|
|
475
|
+
{
|
|
476
|
+
name: rule,
|
|
477
|
+
details,
|
|
478
|
+
recommended: isRecommended,
|
|
479
|
+
fixable: isFixable,
|
|
480
|
+
hasSuggestions,
|
|
481
|
+
version: version,
|
|
482
|
+
contents: mdRuleContents,
|
|
483
|
+
sources: mdRuleSources,
|
|
484
|
+
construction: underConstruction
|
|
485
|
+
},
|
|
486
|
+
];
|
|
487
|
+
}
|
|
479
488
|
}
|
|
480
489
|
}
|
|
481
490
|
}
|
|
482
491
|
});
|
|
492
|
+
return ruleDict;
|
|
493
|
+
},
|
|
483
494
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
495
|
+
getRuleExamples: function (ruleTestPath, testPath, ruleDictEntry) {
|
|
496
|
+
// Get rule valid/invalid tests
|
|
497
|
+
let mdRule = "";
|
|
498
|
+
if (fs.existsSync(ruleTestPath)) {
|
|
499
|
+
const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
|
|
500
|
+
const filename = module.exports.getKeyFromTest(
|
|
501
|
+
ruleTest,
|
|
502
|
+
"filename"
|
|
503
|
+
);
|
|
504
|
+
let errorsString = module.exports.getKeyFromTest(
|
|
505
|
+
ruleTest,
|
|
506
|
+
"errors"
|
|
507
|
+
);
|
|
508
|
+
const re = /(\S+):/gm;
|
|
509
|
+
errorsString = errorsString
|
|
510
|
+
.replace(re, `"$&`)
|
|
511
|
+
.replace(/:/gm, '":')
|
|
512
|
+
.replace(/`/gm, '"');
|
|
513
|
+
const errors = JSONC.parse(`{${errorsString}}`).errors;
|
|
514
|
+
const valid = fs.readFileSync(
|
|
515
|
+
path.join(testPath, ruleDictEntry.name, "valid", filename),
|
|
516
|
+
"utf8"
|
|
517
|
+
);
|
|
518
|
+
let invalid = fs.readFileSync(
|
|
519
|
+
path.join(testPath, ruleDictEntry.name, "invalid", filename),
|
|
520
|
+
"utf8"
|
|
521
|
+
);
|
|
522
|
+
const insertAt = (str, sub, pos) =>
|
|
523
|
+
`${str.slice(0, pos)}${sub}${str.slice(pos)}`;
|
|
524
|
+
let errorsSorted = []
|
|
525
|
+
errors.forEach((err) => {
|
|
526
|
+
if (errorsSorted.length === 0) {
|
|
527
|
+
errorsSorted = [err];
|
|
528
|
+
} else {
|
|
529
|
+
const errLast = errorsSorted[errorsSorted.length - 1];
|
|
530
|
+
if (err.line > errLast.line) {
|
|
531
|
+
errorsSorted.push(err)
|
|
532
|
+
} else if (err.line < errLast.line) {
|
|
533
|
+
errorsSorted.unshift(err);
|
|
534
|
+
} else {
|
|
535
|
+
if (err.column > errLast.column) {
|
|
536
|
+
errorsSorted.push(err)
|
|
537
|
+
} else if (err.line < errLast.line) {
|
|
538
|
+
errorsSorted.unshift(err)
|
|
539
|
+
} else {
|
|
540
|
+
errorsSorted.push(err)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
errorsSorted.reverse().forEach((err, i) => {
|
|
546
|
+
if (err.messageId) {
|
|
547
|
+
let msg = ruleDictEntry.messages[err.messageId];
|
|
548
|
+
let data;
|
|
549
|
+
if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
|
|
550
|
+
data = errorsSorted[i].suggestions[0].data;
|
|
551
|
+
}
|
|
552
|
+
if (data) {
|
|
553
|
+
Object.keys(data).forEach((d) => {
|
|
554
|
+
msg = msg.replace(`{{${d}}}`, data[d]);
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
err.message = msg;
|
|
558
|
+
}
|
|
559
|
+
const msg = err.message.replace(/"/gm, "`");
|
|
560
|
+
if (err.line) {
|
|
561
|
+
const code = invalid.split("\n");
|
|
562
|
+
code[err.line - 1] = insertAt(
|
|
563
|
+
code[err.line - 1],
|
|
564
|
+
"</span>",
|
|
565
|
+
err.endColumn - 1
|
|
566
|
+
);
|
|
567
|
+
code[err.line - 1] = insertAt(
|
|
568
|
+
code[err.line - 1],
|
|
569
|
+
`<span style="display:inline-block; position:relative; border-bottom:2pt dotted red" title="${msg}">`,
|
|
570
|
+
err.column - 1
|
|
571
|
+
);
|
|
572
|
+
invalid = code.join("\n");
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
mdRule +=
|
|
577
|
+
`<span>✔️ Example of ` +
|
|
578
|
+
`<span style="color:green">correct</span> ` +
|
|
579
|
+
`code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
|
|
580
|
+
mdRule +=
|
|
581
|
+
`<span>❌ Example of ` +
|
|
582
|
+
`<span style="color:red">incorrect</span> ` +
|
|
583
|
+
`code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
|
|
487
584
|
}
|
|
585
|
+
return mdRule;
|
|
488
586
|
},
|
|
489
587
|
|
|
490
588
|
/**
|
|
@@ -501,10 +599,14 @@ module.exports = {
|
|
|
501
599
|
fs.readdirSync(dirname).forEach((file) => {
|
|
502
600
|
if (path.extname(file) === ".js" && file !== "index.js") {
|
|
503
601
|
const rulename = file.replace(".js", "");
|
|
504
|
-
|
|
505
|
-
|
|
602
|
+
let rule = require(path.join(dirname, file));
|
|
603
|
+
if (!rule.meta) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
rule = module.exports.applyRuleDefaults(rule);
|
|
607
|
+
rules[rulename] = rule;
|
|
506
608
|
const category =
|
|
507
|
-
rules[
|
|
609
|
+
rules[rulename].meta.docs.category || categories["model"];
|
|
508
610
|
if (
|
|
509
611
|
!listEnvRules.includes(rulename) &&
|
|
510
612
|
category === categories["env"]
|
|
@@ -523,6 +625,32 @@ module.exports = {
|
|
|
523
625
|
return { rules, listRules, listEnvRules, listModelRules };
|
|
524
626
|
},
|
|
525
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Sets defaults for rule meta data for missing required
|
|
630
|
+
* propeties:
|
|
631
|
+
* - Rule is of type "problem"
|
|
632
|
+
* - Rule is in model category
|
|
633
|
+
* - Rule severity is "error"
|
|
634
|
+
* @param {*} rule
|
|
635
|
+
* @returns
|
|
636
|
+
*/
|
|
637
|
+
applyRuleDefaults(rule) {
|
|
638
|
+
let ruleSanitized;
|
|
639
|
+
if (rule.meta) {
|
|
640
|
+
ruleSanitized = { ...rule };
|
|
641
|
+
if (!rule.meta.type) {
|
|
642
|
+
ruleSanitized.meta.type = "problem";
|
|
643
|
+
}
|
|
644
|
+
if (rule.meta.docs && !rule.meta.docs.category) {
|
|
645
|
+
ruleSanitized.meta.docs.category = categories["model"];
|
|
646
|
+
}
|
|
647
|
+
if (rule.meta.docs.recommended && !rule.meta.severity) {
|
|
648
|
+
ruleSanitized.meta.severity = "error";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return ruleSanitized;
|
|
652
|
+
},
|
|
653
|
+
|
|
526
654
|
populateRules: function (context, customRulesDir) {
|
|
527
655
|
const configPath = Cache.get("configpath") || "";
|
|
528
656
|
// Allow for custom rules
|