@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.
@@ -101,20 +101,22 @@ module.exports = {
101
101
  module.exports.Cache.set("filepath", context.filePath);
102
102
 
103
103
  // Get CDS reflected model
104
- if (isValidFile(context.filePath, "model")) {
105
- if (
106
- process.env["LINT_FLAVOR"] !== "parsed" &&
107
- (module.exports.isNewFile(context.filePath) ||
104
+
105
+ if (
106
+ process.env["LINT_FLAVOR"] !== "parsed" &&
107
+ (module.exports.isNewFile(context.filePath) ||
108
108
  module.exports.isNewConfigPath(context.configPath))
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 (module.exports.hasFileChanged(context) || process.env["LINT_FLAVOR"] === "parsed") {
116
- module.exports.updateModel(context);
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
- return true;
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(context, files) {
482
- if (files && files.length > 0 && files.includes(context.filePath)) {
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 (!process.env["LINT_FLAVOR"] === "parsed" || module.exports.isFileInModel(context, files)) {
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
  };
@@ -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, recommended } = require("../constants");
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 version = "latest";
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
- mdRules += "| | | | | |\n";
254
- mdRules += "|:-:|:-:|:-:|:-:|:-|\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
+ }
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 (!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);
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
- const ruleDict = {};
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 = 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
- );
411
+ const ruleMeta = require(path.join(rulePath, file)).meta;
412
+ const version = ruleMeta.docs.version;
367
413
 
368
- let isFixable = "";
369
- if (["code", "whitespace"].includes(fixable)) {
370
- isFixable = "🔧";
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
- 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");
423
+ let underConstruction = "";
424
+ if (!release && semver.satisfies(version, `>${versionRequired}`)) {
425
+ underConstruction = "🚧";
426
+ }
384
427
 
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
- });
428
+ let isFixable = "";
429
+ if (["code", "whitespace"].includes(fixable)) {
430
+ isFixable = "🔧";
431
+ }
433
432
 
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
- }
433
+ let isRecommended = "";
434
+ if (recommended === true) {
435
+ isRecommended = "✔️";
436
+ }
443
437
 
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`;
438
+ let hasSuggestions = "";
439
+ if (suggestions === true) {
440
+ hasSuggestions = "💡";
441
+ }
454
442
 
455
- if (Object.keys(ruleDict).includes(category)) {
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
- 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
- ];
452
+ };
453
+ mdRule = module.exports.getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
454
+ mdRuleContents = "";
455
+ if (!release && underConstruction) {
456
+ mdRuleContents += `## ${rule}\n<span class='shifted'>${underConstruction}&nbsp;&nbsp;<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
- module.exports.genDocFiles(ruleDict, docsPath);
485
- if (release) {
486
- module.exports.genDocFiles(ruleDict, docsPath, release);
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>✔️&nbsp;&nbsp; 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>❌&nbsp;&nbsp; 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
- const ruleID = `${rulename}`;
505
- rules[ruleID] = require(path.join(dirname, file));
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[ruleID].meta.docs.category || categories["model"];
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