@sap/eslint-plugin-cds 2.1.0 → 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 +51 -1
- package/README.md +1 -1
- package/lib/api/formatter.js +165 -188
- package/lib/api/index.js +22 -7
- package/lib/impl/constants.js +5 -24
- package/lib/impl/index.js +53 -13
- package/lib/impl/parser.js +12 -4
- package/lib/impl/processor.js +23 -0
- package/lib/impl/ruleFactory.js +250 -272
- package/lib/impl/rules/assoc2many-ambiguous-key.js +158 -136
- package/lib/impl/rules/cds-compile-error.js +8 -20
- package/lib/impl/rules/latest-cds-version.js +39 -43
- package/lib/impl/rules/min-node-version.js +33 -44
- package/lib/impl/rules/no-db-keywords.js +28 -15
- package/lib/impl/rules/no-join-on-draft-enabled-entities.js +37 -0
- package/lib/impl/rules/require-2many-oncond.js +23 -31
- package/lib/impl/rules/rule.hbs +16 -22
- package/lib/impl/rules/sql-cast-suggestion.js +40 -43
- package/lib/impl/rules/start-elements-lowercase.js +60 -54
- package/lib/impl/rules/start-entities-uppercase.js +44 -54
- package/lib/impl/rules/valid-csv-header.js +92 -0
- package/lib/impl/types.d.ts +48 -0
- package/lib/impl/utils/fuzzySearch.js +87 -0
- package/lib/impl/utils/helpers.js +32 -9
- package/lib/impl/utils/model.js +326 -172
- package/lib/impl/utils/rules.js +472 -251
- package/lib/impl/utils/validate.js +52 -0
- package/package.json +2 -2
- package/lib/impl/rules/index.js +0 -5
- package/lib/impl/rules/test.hbs +0 -10
package/lib/impl/utils/rules.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef { import('eslint').Linter.ConfigOverride.files } ConfigOverrideFiles
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const cp = require("child_process");
|
|
8
|
+
const semver = require("semver");
|
|
2
9
|
const path = require("path");
|
|
3
10
|
const { mkdirp } = require("@sap/cds/lib/utils");
|
|
4
|
-
const { getLastLine } = require("./model");
|
|
11
|
+
const { Cache, getLastLine } = require("./model");
|
|
5
12
|
|
|
6
13
|
const JSONC = require("./jsonc");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
14
|
+
const { categories } = require("../constants");
|
|
15
|
+
const IS_WIN = os.platform() === "win32";
|
|
16
|
+
const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
|
|
9
17
|
const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
|
|
10
18
|
|
|
11
19
|
module.exports = {
|
|
12
20
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @returns Array of file extensions
|
|
16
|
-
*/
|
|
17
|
-
getFileExtensions: function () {
|
|
18
|
-
return CONSTANTS.overrides[0].files;
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Turns rules on/off for given line according to eslint-disable comments:
|
|
21
|
+
* Turns rules "on" or "off" for given line according to eslint-disable
|
|
22
|
+
* comments:
|
|
23
23
|
* 1. Reads code string and extracts a list of comments (in order)
|
|
24
24
|
* 2. Initiates rulesDisabled array with all rules "on" by default
|
|
25
25
|
* 3. Switches rules "off" (or "on" again) based on disable comment
|
|
@@ -28,92 +28,140 @@ module.exports = {
|
|
|
28
28
|
* @param line current code line to analyze
|
|
29
29
|
* @returns rules dictionary with rules being either 'on' and 'off'
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
getDisabled: function (code, sourcecode, line) {
|
|
32
32
|
const listDisabled = [];
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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}`);
|
|
33
|
+
let { listEnvRules, listModelRules, listRules } = Cache.get("rulesInfo");
|
|
34
|
+
const rulesDisabled = listRules.reduce(
|
|
35
|
+
(o, key) => ({ ...o, [key]: "on" }),
|
|
36
|
+
{}
|
|
37
|
+
);
|
|
38
|
+
let matches = [];
|
|
39
|
+
if (code) {
|
|
40
|
+
matches = [...code.matchAll(REGEX_COMMENTS)];
|
|
41
|
+
if (matches.length > 0) {
|
|
42
|
+
matches.forEach((match) => {
|
|
43
|
+
if (match) {
|
|
44
|
+
const index = match.index;
|
|
45
|
+
match = match[0];
|
|
46
|
+
if (match.includes("*/")) {
|
|
47
|
+
match = match.split("*/")[0].replace("/*", "");
|
|
48
|
+
} else if (match.includes("//")) {
|
|
49
|
+
match = match.split("//")[1];
|
|
50
|
+
}
|
|
51
|
+
if (match) {
|
|
52
|
+
match = match.trim();
|
|
56
53
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
[
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
};
|
|
54
|
+
["disable", "enable"].forEach((keyword) => {
|
|
55
|
+
const loc = sourcecode.getLocFromIndex(index);
|
|
56
|
+
const disableType = match.split(" ")[0];
|
|
57
|
+
let disableRules = match.split(`${disableType} `)[1];
|
|
58
|
+
if (disableRules) {
|
|
59
|
+
disableRules = disableRules
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((rule) => rule.trim());
|
|
72
62
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
rules: disableRules,
|
|
77
|
-
type: keyword,
|
|
78
|
-
};
|
|
63
|
+
disableRules = listEnvRules
|
|
64
|
+
.concat(listModelRules)
|
|
65
|
+
.map((rule) => `@sap/cds/${rule}`);
|
|
79
66
|
}
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
let comment = {};
|
|
68
|
+
if (
|
|
69
|
+
[
|
|
70
|
+
`eslint-${keyword}`,
|
|
71
|
+
`eslint-${keyword}-line`,
|
|
72
|
+
`eslint-${keyword}-next-line`,
|
|
73
|
+
].includes(disableType)
|
|
74
|
+
) {
|
|
75
|
+
if (disableType.includes("-next-line")) {
|
|
76
|
+
comment = {
|
|
77
|
+
lineComment: loc.line,
|
|
78
|
+
lineDisabled: loc.line + 1,
|
|
79
|
+
rules: disableRules,
|
|
80
|
+
type: keyword,
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
comment = {
|
|
84
|
+
lineComment: loc.line,
|
|
85
|
+
lineDisabled: loc.line,
|
|
86
|
+
rules: disableRules,
|
|
87
|
+
type: keyword,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!disableType.includes("-line")) {
|
|
91
|
+
comment.lineDisabled = "EOF";
|
|
92
|
+
}
|
|
82
93
|
}
|
|
94
|
+
listDisabled.push(comment);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
for (let i = 0; i <= listDisabled.length - 1; i++) {
|
|
99
|
+
if (listDisabled[i].lineComment > line) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
listDisabled[i].lineDisabled === "EOF" ||
|
|
104
|
+
listDisabled[i].lineDisabled === line
|
|
105
|
+
) {
|
|
106
|
+
if (listDisabled[i].lineDisabled === "EOF") {
|
|
107
|
+
listDisabled[i].lineDisabled = getLastLine(code);
|
|
83
108
|
}
|
|
84
|
-
listDisabled.
|
|
85
|
-
|
|
109
|
+
if (listDisabled[i].rules) {
|
|
110
|
+
listDisabled[i].rules.forEach((rule) => {
|
|
111
|
+
if (listDisabled[i].type === "disable") {
|
|
112
|
+
rulesDisabled[rule] = "off";
|
|
113
|
+
} else if (listDisabled[i].type === "enable") {
|
|
114
|
+
rulesDisabled[rule] = "on";
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
86
119
|
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return rulesDisabled;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks whether a lint rule has been disabled by eslint-disable
|
|
127
|
+
* comments at a given location
|
|
128
|
+
* @param entry lint report
|
|
129
|
+
* @param cdscontext cds context object
|
|
130
|
+
* @param rules all availabe rules
|
|
131
|
+
* @returns boolean
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
isRuleDisabled: function (entry, cdscontext) {
|
|
135
|
+
let isDisabled = false;
|
|
136
|
+
if (entry.loc && entry.loc.start) {
|
|
137
|
+
const line = entry.loc.start.line;
|
|
138
|
+
if (cdscontext) {
|
|
139
|
+
const rulesDisabled = module.exports.getDisabled(
|
|
140
|
+
cdscontext.code,
|
|
141
|
+
cdscontext.sourcecode,
|
|
142
|
+
line
|
|
143
|
+
);
|
|
144
|
+
let ruleID = cdscontext.ruleID;
|
|
145
|
+
if (!ruleID) {
|
|
146
|
+
ruleID = cdscontext.id;
|
|
91
147
|
}
|
|
92
148
|
if (
|
|
93
|
-
|
|
94
|
-
|
|
149
|
+
line &&
|
|
150
|
+
ruleID in rulesDisabled &&
|
|
151
|
+
rulesDisabled[ruleID] === "off"
|
|
95
152
|
) {
|
|
96
|
-
|
|
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
|
-
}
|
|
153
|
+
isDisabled = true;
|
|
108
154
|
}
|
|
109
155
|
}
|
|
110
156
|
}
|
|
111
|
-
return
|
|
157
|
+
return isDisabled;
|
|
112
158
|
},
|
|
159
|
+
|
|
113
160
|
/**
|
|
114
|
-
* Gets value for a given key in allowed keys of ESLint's meta data in
|
|
115
|
-
*
|
|
116
|
-
* @param {
|
|
161
|
+
* Gets value for a given key in allowed keys of ESLint's meta data in
|
|
162
|
+
* defineRule api
|
|
163
|
+
* @param {string} text meta object from rule
|
|
164
|
+
* @param {string} key key to get value for
|
|
117
165
|
* @returns Value for given key
|
|
118
166
|
*/
|
|
119
167
|
getKeyFromMeta: function (text, key) {
|
|
@@ -138,14 +186,44 @@ module.exports = {
|
|
|
138
186
|
return "";
|
|
139
187
|
}
|
|
140
188
|
} else {
|
|
189
|
+
const regexBoolean = new RegExp(`${key}:[\\s]+true[\\s]?,`, "gm");
|
|
190
|
+
const matchBoolean = regexBoolean.exec(text);
|
|
191
|
+
if (matchBoolean) {
|
|
192
|
+
if (matchBoolean[0].includes("true,")) {
|
|
193
|
+
return true;
|
|
194
|
+
} else {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
141
198
|
return "";
|
|
142
199
|
}
|
|
143
200
|
},
|
|
144
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
|
+
|
|
145
223
|
/**
|
|
146
224
|
* Gets value for a given key in allowed keys for input of runRuleTester api
|
|
147
|
-
* @param {
|
|
148
|
-
* @param {
|
|
225
|
+
* @param {string} text test input for ruleTester
|
|
226
|
+
* @param {string} key key to get value for
|
|
149
227
|
* @returns Value for given key
|
|
150
228
|
*/
|
|
151
229
|
getKeyFromTest: function (text, key) {
|
|
@@ -172,6 +250,12 @@ module.exports = {
|
|
|
172
250
|
if (matchTestKey) {
|
|
173
251
|
result = matchTestKey[0];
|
|
174
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
|
+
}
|
|
175
259
|
} else {
|
|
176
260
|
result = `No parameter \\'${key}\\' found in ruleTest`;
|
|
177
261
|
}
|
|
@@ -180,26 +264,37 @@ module.exports = {
|
|
|
180
264
|
|
|
181
265
|
/**
|
|
182
266
|
* Generates overview table of all rules based on rule dictionary.
|
|
183
|
-
* @param
|
|
184
|
-
* @param
|
|
185
|
-
* @param
|
|
267
|
+
* @param ruleDict
|
|
268
|
+
* @param release
|
|
269
|
+
* @param table
|
|
186
270
|
* @returns Markdown table
|
|
187
271
|
*/
|
|
188
272
|
genMdRules: function (ruleDict, release, table = true) {
|
|
189
|
-
let
|
|
190
|
-
if (release) {
|
|
191
|
-
version = release;
|
|
192
|
-
}
|
|
193
|
-
let mdRules = `# @sap/eslint-plugin-cds [${version}]\n\n`;
|
|
273
|
+
let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
|
|
194
274
|
if (table) {
|
|
195
|
-
mdRules +=
|
|
196
|
-
mdRules += "
|
|
197
|
-
|
|
275
|
+
mdRules += `Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n`;
|
|
276
|
+
mdRules += `✔️ if the plugin's "recommended" configuration enables the rule\n\n`;
|
|
277
|
+
mdRules += `🔧 if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`;
|
|
278
|
+
mdRules += `💡 if problems reported by the rule are manually fixable (editor)\n\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
|
+
}
|
|
287
|
+
/* eslint-disable-next-line no-unused-vars */
|
|
288
|
+
Object.entries(ruleDict).forEach(([, rules]) => {
|
|
198
289
|
rules.forEach(function (rule) {
|
|
199
|
-
|
|
290
|
+
if (release) {
|
|
291
|
+
mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | | | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`;
|
|
292
|
+
} else {
|
|
293
|
+
mdRules += `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | ${rule.construction} | | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
|
|
294
|
+
}
|
|
200
295
|
});
|
|
201
296
|
});
|
|
202
|
-
mdRules += "
|
|
297
|
+
mdRules += "\n";
|
|
203
298
|
}
|
|
204
299
|
return mdRules;
|
|
205
300
|
},
|
|
@@ -211,11 +306,11 @@ module.exports = {
|
|
|
211
306
|
* If used internally within the @sap/eslint-plugin-cds, this
|
|
212
307
|
* also generates 'released' files, which only contain information
|
|
213
308
|
* on rules published until the currently released version.
|
|
214
|
-
* @param
|
|
215
|
-
* @param
|
|
216
|
-
* @param
|
|
309
|
+
* @param ruleDict
|
|
310
|
+
* @param docsPath
|
|
311
|
+
* @param release
|
|
217
312
|
*/
|
|
218
|
-
genDocFiles: function (ruleDict, docsPath, release) {
|
|
313
|
+
genDocFiles: function (ruleDict, docsPath, release=false) {
|
|
219
314
|
let suffix = "";
|
|
220
315
|
if (release) {
|
|
221
316
|
suffix = "-released";
|
|
@@ -232,8 +327,11 @@ module.exports = {
|
|
|
232
327
|
const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
|
|
233
328
|
const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
|
|
234
329
|
|
|
330
|
+
// Get rules table
|
|
331
|
+
let mdRuleList = module.exports.genMdRules(ruleDict, release, true);
|
|
332
|
+
|
|
333
|
+
// Get rule details
|
|
235
334
|
let mdRules = module.exports.genMdRules(ruleDict, release, false);
|
|
236
|
-
let mdRuleList = module.exports.genMdRules(ruleDict);
|
|
237
335
|
/* eslint-disable-next-line no-unused-vars */
|
|
238
336
|
Object.entries(ruleDict).forEach(([category, rules]) => {
|
|
239
337
|
rules.forEach(function (rule) {
|
|
@@ -252,10 +350,10 @@ module.exports = {
|
|
|
252
350
|
* for user according to contents of:
|
|
253
351
|
* - Rule files
|
|
254
352
|
* - Test files (with valid/invalid/fixed examples)
|
|
255
|
-
* @param {
|
|
353
|
+
* @param {string} projectPath
|
|
354
|
+
* @param {string} customRulesDir
|
|
256
355
|
*/
|
|
257
|
-
async genDocs(projectPath, customRulesDir) {
|
|
258
|
-
let mdRule, mdRuleSources, mdRuleContents;
|
|
356
|
+
async genDocs(projectPath, customRulesDir, registry) {
|
|
259
357
|
let docsPath, rulePath, testPath, release;
|
|
260
358
|
|
|
261
359
|
if (!projectPath) {
|
|
@@ -269,19 +367,40 @@ module.exports = {
|
|
|
269
367
|
docsPath = path.join(projectPath, `${customRulesDir}/docs`);
|
|
270
368
|
rulePath = path.join(projectPath, `${customRulesDir}/rules`);
|
|
271
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
|
+
}
|
|
272
379
|
}
|
|
273
380
|
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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);
|
|
282
398
|
}
|
|
399
|
+
},
|
|
283
400
|
|
|
284
|
-
|
|
401
|
+
getRuleDict: function (docsPath, rulePath, testPath, versionRequired='0.0.0', release=false) {
|
|
402
|
+
let mdRule, mdRuleSources, mdRuleContents;
|
|
403
|
+
let ruleDict = {};
|
|
285
404
|
fs.readdirSync(rulePath).filter(function (file) {
|
|
286
405
|
if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
|
|
287
406
|
const rule = path.basename(file).replace(path.extname(file), "");
|
|
@@ -289,169 +408,271 @@ module.exports = {
|
|
|
289
408
|
const ruleTestPath = path.join(testPath, rule, "rule.test.js");
|
|
290
409
|
|
|
291
410
|
// Get rule meta information
|
|
292
|
-
const ruleMeta =
|
|
293
|
-
const
|
|
294
|
-
ruleMeta,
|
|
295
|
-
"description"
|
|
296
|
-
);
|
|
297
|
-
const category = module.exports.getKeyFromMeta(ruleMeta, "category");
|
|
298
|
-
const fixable = module.exports.getKeyFromMeta(ruleMeta, "fixable");
|
|
411
|
+
const ruleMeta = require(path.join(rulePath, file)).meta;
|
|
412
|
+
const version = ruleMeta.docs.version;
|
|
299
413
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
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;
|
|
309
422
|
|
|
310
|
-
|
|
423
|
+
let underConstruction = "";
|
|
424
|
+
if (!release && semver.satisfies(version, `>${versionRequired}`)) {
|
|
425
|
+
underConstruction = "🚧";
|
|
426
|
+
}
|
|
311
427
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const filename = module.exports.getKeyFromTest(
|
|
317
|
-
ruleTest,
|
|
318
|
-
"filename"
|
|
319
|
-
);
|
|
320
|
-
let errorsString = module.exports.getKeyFromTest(
|
|
321
|
-
ruleTest,
|
|
322
|
-
"errors"
|
|
323
|
-
);
|
|
324
|
-
const re = /(\S+):/gm;
|
|
325
|
-
errorsString = errorsString
|
|
326
|
-
.replace(re, `"$&`)
|
|
327
|
-
.replace(/:/gm, '":')
|
|
328
|
-
.replace(/`/gm, '"');
|
|
329
|
-
const errors = JSONC.parse(`{${errorsString}}`).errors;
|
|
330
|
-
const valid = fs.readFileSync(
|
|
331
|
-
path.join(testPath, rule, "valid", filename),
|
|
332
|
-
"utf8"
|
|
333
|
-
);
|
|
334
|
-
let invalid = fs.readFileSync(
|
|
335
|
-
path.join(testPath, rule, "invalid", filename),
|
|
336
|
-
"utf8"
|
|
337
|
-
);
|
|
338
|
-
const insertAt = (str, sub, pos) =>
|
|
339
|
-
`${str.slice(0, pos)}${sub}${str.slice(pos)}`;
|
|
340
|
-
errors.forEach((err) => {
|
|
341
|
-
const msg = err.message.replace(/"/gm, "`");
|
|
342
|
-
if (err.line) {
|
|
343
|
-
const code = invalid.split("\n");
|
|
344
|
-
code[err.line - 1] = insertAt(
|
|
345
|
-
code[err.line - 1],
|
|
346
|
-
"</span>",
|
|
347
|
-
err.endColumn - 1
|
|
348
|
-
);
|
|
349
|
-
code[err.line - 1] = insertAt(
|
|
350
|
-
code[err.line - 1],
|
|
351
|
-
`<span style="text-decoration-line:underline; text-decoration-style:wavy; text-decoration-color:red;" title="${msg}">`,
|
|
352
|
-
err.column - 1
|
|
353
|
-
);
|
|
354
|
-
invalid = code.join("\n");
|
|
355
|
-
}
|
|
356
|
-
});
|
|
428
|
+
let isFixable = "";
|
|
429
|
+
if (["code", "whitespace"].includes(fixable)) {
|
|
430
|
+
isFixable = "🔧";
|
|
431
|
+
}
|
|
357
432
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
mdRule +=
|
|
363
|
-
`<span>❌ Example of ` +
|
|
364
|
-
`<span style="color:red">incorrect</span> ` +
|
|
365
|
-
`code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
|
|
366
|
-
}
|
|
433
|
+
let isRecommended = "";
|
|
434
|
+
if (recommended === true) {
|
|
435
|
+
isRecommended = "✔️";
|
|
436
|
+
}
|
|
367
437
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
mdRuleContents += `### Examples\n${mdRule}\n\n`;
|
|
373
|
-
}
|
|
374
|
-
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`;
|
|
438
|
+
let hasSuggestions = "";
|
|
439
|
+
if (suggestions === true) {
|
|
440
|
+
hasSuggestions = "💡";
|
|
441
|
+
}
|
|
379
442
|
|
|
380
|
-
|
|
381
|
-
ruleDict[category].push({
|
|
443
|
+
const ruleDictEntry = {
|
|
382
444
|
name: rule,
|
|
383
445
|
details,
|
|
384
446
|
recommended: isRecommended,
|
|
385
|
-
|
|
447
|
+
fixable: isFixable,
|
|
448
|
+
hasSuggestions,
|
|
449
|
+
construction: underConstruction,
|
|
450
|
+
messages,
|
|
386
451
|
version: version,
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
}
|
|
402
488
|
}
|
|
403
489
|
}
|
|
404
490
|
}
|
|
405
491
|
});
|
|
492
|
+
return ruleDict;
|
|
493
|
+
},
|
|
406
494
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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>`;
|
|
410
584
|
}
|
|
585
|
+
return mdRule;
|
|
411
586
|
},
|
|
412
587
|
|
|
413
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Gets all relevant rules information (dictionary of rules contents, lists
|
|
590
|
+
* of 'env' and 'model' rules) for the rules directory provided
|
|
591
|
+
* - Default categories is 'model'
|
|
592
|
+
* @param {string} dirname
|
|
593
|
+
* @returns rules information
|
|
594
|
+
*/
|
|
595
|
+
getRules(dirname) {
|
|
414
596
|
const rules = {};
|
|
597
|
+
const listEnvRules = [];
|
|
598
|
+
const listModelRules = [];
|
|
415
599
|
fs.readdirSync(dirname).forEach((file) => {
|
|
416
600
|
if (path.extname(file) === ".js" && file !== "index.js") {
|
|
417
|
-
|
|
601
|
+
const rulename = file.replace(".js", "");
|
|
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;
|
|
608
|
+
const category =
|
|
609
|
+
rules[rulename].meta.docs.category || categories["model"];
|
|
610
|
+
if (
|
|
611
|
+
!listEnvRules.includes(rulename) &&
|
|
612
|
+
category === categories["env"]
|
|
613
|
+
) {
|
|
614
|
+
listEnvRules.push(rulename);
|
|
615
|
+
}
|
|
616
|
+
if (
|
|
617
|
+
!listModelRules.includes(rulename) &&
|
|
618
|
+
category === categories["model"]
|
|
619
|
+
) {
|
|
620
|
+
listModelRules.push(rulename);
|
|
621
|
+
}
|
|
418
622
|
}
|
|
419
623
|
});
|
|
420
|
-
|
|
624
|
+
const listRules = listEnvRules.concat(listModelRules);
|
|
625
|
+
return { rules, listRules, listEnvRules, listModelRules };
|
|
421
626
|
},
|
|
422
627
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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";
|
|
438
643
|
}
|
|
439
|
-
|
|
440
|
-
|
|
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;
|
|
441
652
|
},
|
|
442
653
|
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
654
|
+
populateRules: function (context, customRulesDir) {
|
|
655
|
+
const configPath = Cache.get("configpath") || "";
|
|
656
|
+
// Allow for custom rules
|
|
657
|
+
if (configPath) {
|
|
658
|
+
let customRulesPath = path.join(
|
|
659
|
+
Cache.get("configpath"),
|
|
660
|
+
customRulesDir,
|
|
661
|
+
"rules"
|
|
662
|
+
);
|
|
663
|
+
let customRulesInfo;
|
|
664
|
+
if (fs.existsSync(customRulesPath)) {
|
|
665
|
+
customRulesInfo = module.exports.getRules(customRulesPath);
|
|
666
|
+
Cache.set("rulesInfo", {
|
|
667
|
+
listEnvRules: context.listEnvRules.concat(
|
|
668
|
+
customRulesInfo.listEnvRules
|
|
669
|
+
),
|
|
670
|
+
listModelRules: context.listModelRules.concat(
|
|
671
|
+
customRulesInfo.listModelRules
|
|
672
|
+
),
|
|
673
|
+
listRules: context.listRules.concat(customRulesInfo.listRules),
|
|
674
|
+
});
|
|
453
675
|
}
|
|
454
|
-
}
|
|
455
|
-
return modelRules;
|
|
676
|
+
}
|
|
456
677
|
},
|
|
457
678
|
};
|