@sap/eslint-plugin-cds 2.3.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.
- package/CHANGELOG.md +11 -50
- package/lib/api/index.js +11 -13
- package/lib/api/lint.d.ts +48 -0
- package/lib/constants.js +54 -0
- package/lib/index.js +44 -0
- package/lib/{impl/parser.js → parser.js} +2 -13
- package/lib/processor.js +47 -0
- package/lib/{impl/rules → rules}/assoc2many-ambiguous-key.js +50 -53
- package/lib/rules/latest-cds-version.js +42 -0
- package/lib/rules/min-node-version.js +47 -0
- package/lib/rules/no-db-keywords.js +46 -0
- package/lib/rules/no-dollar-prefixed-names.js +47 -0
- package/lib/{impl/rules → rules}/no-join-on-draft-enabled-entities.js +14 -11
- package/lib/rules/require-2many-oncond.js +27 -0
- package/lib/rules/sql-cast-suggestion.js +52 -0
- package/lib/rules/start-elements-lowercase.js +61 -0
- package/lib/rules/start-entities-uppercase.js +55 -0
- package/lib/{impl/rules → rules}/valid-csv-header.js +17 -9
- package/lib/{impl/utils → utils}/fuzzySearch.js +0 -0
- package/lib/utils/helpers.js +55 -0
- package/lib/{impl/utils → utils}/jsonc.js +0 -0
- package/lib/{impl/utils → utils}/model.js +107 -221
- package/lib/utils/ruleHelpers.js +56 -0
- package/lib/utils/ruleTester.js +79 -0
- package/lib/utils/rules.js +1033 -0
- package/lib/{impl/utils → utils}/validate.js +2 -18
- package/package.json +2 -2
- package/lib/impl/constants.js +0 -30
- package/lib/impl/index.js +0 -63
- package/lib/impl/processor.js +0 -23
- package/lib/impl/ruleFactory.js +0 -360
- package/lib/impl/rules/cds-compile-error.js +0 -34
- package/lib/impl/rules/latest-cds-version.js +0 -51
- package/lib/impl/rules/min-node-version.js +0 -44
- package/lib/impl/rules/no-db-keywords.js +0 -38
- package/lib/impl/rules/require-2many-oncond.js +0 -31
- package/lib/impl/rules/rule.hbs +0 -20
- package/lib/impl/rules/sql-cast-suggestion.js +0 -52
- package/lib/impl/rules/start-elements-lowercase.js +0 -75
- package/lib/impl/rules/start-entities-uppercase.js +0 -65
- package/lib/impl/types.d.ts +0 -48
- package/lib/impl/utils/helpers.js +0 -68
- package/lib/impl/utils/rules.js +0 -697
package/lib/impl/utils/rules.js
DELETED
|
@@ -1,697 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef { import('eslint').Linter.ConfigOverride.files } ConfigOverrideFiles
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const os = require("os");
|
|
7
|
-
const cp = require("child_process");
|
|
8
|
-
const semver = require("semver");
|
|
9
|
-
const path = require("path");
|
|
10
|
-
const { mkdirp } = require("@sap/cds/lib/utils");
|
|
11
|
-
const { Cache, getLastLine } = require("./model");
|
|
12
|
-
|
|
13
|
-
const JSONC = require("./jsonc");
|
|
14
|
-
const { categories } = require("../constants");
|
|
15
|
-
const IS_WIN = os.platform() === "win32";
|
|
16
|
-
const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
|
|
17
|
-
const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
|
|
18
|
-
|
|
19
|
-
module.exports = {
|
|
20
|
-
/**
|
|
21
|
-
* Turns rules "on" or "off" for given line according to eslint-disable
|
|
22
|
-
* comments:
|
|
23
|
-
* 1. Reads code string and extracts a list of comments (in order)
|
|
24
|
-
* 2. Initiates rulesDisabled array with all rules "on" by default
|
|
25
|
-
* 3. Switches rules "off" (or "on" again) based on disable comment
|
|
26
|
-
* @param code current code
|
|
27
|
-
* @param sourcecode source code object to get index from
|
|
28
|
-
* @param line current code line to analyze
|
|
29
|
-
* @returns rules dictionary with rules being either 'on' and 'off'
|
|
30
|
-
*/
|
|
31
|
-
getDisabled: function (code, sourcecode, line) {
|
|
32
|
-
const listDisabled = [];
|
|
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();
|
|
53
|
-
}
|
|
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());
|
|
62
|
-
} else {
|
|
63
|
-
disableRules = listEnvRules
|
|
64
|
-
.concat(listModelRules)
|
|
65
|
-
.map((rule) => `@sap/cds/${rule}`);
|
|
66
|
-
}
|
|
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
|
-
}
|
|
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);
|
|
108
|
-
}
|
|
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
|
-
}
|
|
119
|
-
}
|
|
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;
|
|
147
|
-
}
|
|
148
|
-
if (
|
|
149
|
-
line &&
|
|
150
|
-
ruleID in rulesDisabled &&
|
|
151
|
-
rulesDisabled[ruleID] === "off"
|
|
152
|
-
) {
|
|
153
|
-
isDisabled = true;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return isDisabled;
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
/**
|
|
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
|
|
165
|
-
* @returns Value for given key
|
|
166
|
-
*/
|
|
167
|
-
getKeyFromMeta: function (text, key) {
|
|
168
|
-
const regexQuote = new RegExp(`${key}:[\\s]+[\\', \\\`, \\"]`, "gm");
|
|
169
|
-
const matchQuote = regexQuote.exec(text);
|
|
170
|
-
if (matchQuote) {
|
|
171
|
-
const quote = matchQuote[0].slice(-1);
|
|
172
|
-
const exprStart = `${key}:[\\s]+\\${quote}`;
|
|
173
|
-
const exprEnd = `(\\${quote},,?)`;
|
|
174
|
-
const regexKey = new RegExp(`${exprStart}[\\s\\S]*?${exprEnd}`, "gm");
|
|
175
|
-
const matchKey = regexKey.exec(text);
|
|
176
|
-
if (matchKey) {
|
|
177
|
-
const regexStart = new RegExp(`${exprStart}`, "gm");
|
|
178
|
-
const regexEnd = new RegExp(`${exprEnd}`, "gm");
|
|
179
|
-
return matchKey[0]
|
|
180
|
-
.replace(regexStart, "")
|
|
181
|
-
.replace(regexEnd, "")
|
|
182
|
-
.replace("fixable:", "")
|
|
183
|
-
.replace(/\\/gm, "")
|
|
184
|
-
.trim();
|
|
185
|
-
} else {
|
|
186
|
-
return "";
|
|
187
|
-
}
|
|
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
|
-
}
|
|
198
|
-
return "";
|
|
199
|
-
}
|
|
200
|
-
},
|
|
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
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Gets value for a given key in allowed keys for input of runRuleTester api
|
|
225
|
-
* @param {string} text test input for ruleTester
|
|
226
|
-
* @param {string} key key to get value for
|
|
227
|
-
* @returns Value for given key
|
|
228
|
-
*/
|
|
229
|
-
getKeyFromTest: function (text, key) {
|
|
230
|
-
let result = "";
|
|
231
|
-
if (["root", "rule", "filename", "parser"].includes(key)) {
|
|
232
|
-
const regexTestKey = new RegExp(`${key}:.*$`, "gm");
|
|
233
|
-
const matchTestKey = regexTestKey.exec(text);
|
|
234
|
-
if (matchTestKey) {
|
|
235
|
-
const quote = matchTestKey[0].replace(",", "").slice(-1);
|
|
236
|
-
const regexTestValue = new RegExp(
|
|
237
|
-
`${quote}[\\s\\S]*?(\\${quote},?)`,
|
|
238
|
-
"gm"
|
|
239
|
-
);
|
|
240
|
-
const matchValue = regexTestValue.exec(matchTestKey[0]);
|
|
241
|
-
if (matchValue) {
|
|
242
|
-
const regex = new RegExp(`${quote},`, "gm");
|
|
243
|
-
const regex2 = new RegExp(`${quote}`, "gm");
|
|
244
|
-
result = matchValue[0].replace(regex, "").replace(regex2, "");
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
} else if (key === "errors") {
|
|
248
|
-
const regexTestKey = new RegExp(`${key}:.*$(([\\s]+.+)+])?`, "gm");
|
|
249
|
-
const matchTestKey = regexTestKey.exec(text);
|
|
250
|
-
if (matchTestKey) {
|
|
251
|
-
result = matchTestKey[0];
|
|
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
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
result = `No parameter \\'${key}\\' found in ruleTest`;
|
|
261
|
-
}
|
|
262
|
-
return result;
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Generates overview table of all rules based on rule dictionary.
|
|
267
|
-
* @param ruleDict
|
|
268
|
-
* @param release
|
|
269
|
-
* @param table
|
|
270
|
-
* @returns Markdown table
|
|
271
|
-
*/
|
|
272
|
-
genMdRules: function (ruleDict, release, table = true) {
|
|
273
|
-
let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
|
|
274
|
-
if (table) {
|
|
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]) => {
|
|
289
|
-
rules.forEach(function (rule) {
|
|
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
|
-
}
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
mdRules += "\n";
|
|
298
|
-
}
|
|
299
|
-
return mdRules;
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Generates markdown documentation files for:
|
|
304
|
-
* - Overview of all rules in form of markdown table (RuleList)
|
|
305
|
-
* - List of all rules details in form of markdown page (Rules)
|
|
306
|
-
* If used internally within the @sap/eslint-plugin-cds, this
|
|
307
|
-
* also generates 'released' files, which only contain information
|
|
308
|
-
* on rules published until the currently released version.
|
|
309
|
-
* @param ruleDict
|
|
310
|
-
* @param docsPath
|
|
311
|
-
* @param release
|
|
312
|
-
*/
|
|
313
|
-
genDocFiles: function (ruleDict, docsPath, release=false) {
|
|
314
|
-
let suffix = "";
|
|
315
|
-
if (release) {
|
|
316
|
-
suffix = "-released";
|
|
317
|
-
}
|
|
318
|
-
const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`);
|
|
319
|
-
const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`);
|
|
320
|
-
|
|
321
|
-
if (!fs.existsSync(ruleDocsPath)) {
|
|
322
|
-
fs.writeFileSync(ruleDocsPath, "", "utf8");
|
|
323
|
-
}
|
|
324
|
-
if (!fs.existsSync(ruleListDocsPath)) {
|
|
325
|
-
fs.writeFileSync(ruleListDocsPath, "", "utf8");
|
|
326
|
-
}
|
|
327
|
-
const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
|
|
328
|
-
const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
|
|
329
|
-
|
|
330
|
-
// Get rules table
|
|
331
|
-
let mdRuleList = module.exports.genMdRules(ruleDict, release, true);
|
|
332
|
-
|
|
333
|
-
// Get rule details
|
|
334
|
-
let mdRules = module.exports.genMdRules(ruleDict, release, false);
|
|
335
|
-
/* eslint-disable-next-line no-unused-vars */
|
|
336
|
-
Object.entries(ruleDict).forEach(([category, rules]) => {
|
|
337
|
-
rules.forEach(function (rule) {
|
|
338
|
-
mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`;
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
|
|
343
|
-
fs.writeFileSync(ruleDocsPath, mdRules, "utf8");
|
|
344
|
-
fs.writeFileSync(ruleListDocsPath, mdRuleList, "utf8");
|
|
345
|
-
}
|
|
346
|
-
},
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Generates custom rules documentation (markdown files)
|
|
350
|
-
* for user according to contents of:
|
|
351
|
-
* - Rule files
|
|
352
|
-
* - Test files (with valid/invalid/fixed examples)
|
|
353
|
-
* @param {string} projectPath
|
|
354
|
-
* @param {string} customRulesDir
|
|
355
|
-
*/
|
|
356
|
-
async genDocs(projectPath, customRulesDir, registry, prepareRelease = false) {
|
|
357
|
-
let docsPath, rulePath, testPath, release;
|
|
358
|
-
|
|
359
|
-
if (!projectPath) {
|
|
360
|
-
docsPath = path.join(__dirname, "../../../docs");
|
|
361
|
-
rulePath = path.join(__dirname, "../rules");
|
|
362
|
-
testPath = path.join(__dirname, "../../../test/rules");
|
|
363
|
-
release = JSON.parse(
|
|
364
|
-
fs.readFileSync(path.join(__dirname, "../../../package.json"))
|
|
365
|
-
).version;
|
|
366
|
-
} else {
|
|
367
|
-
docsPath = path.join(projectPath, `${customRulesDir}/docs`);
|
|
368
|
-
rulePath = path.join(projectPath, `${customRulesDir}/rules`);
|
|
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
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (registry) {
|
|
382
|
-
// Get rules (internal on artifactory)
|
|
383
|
-
let versionInternal;
|
|
384
|
-
if (!prepareRelease) {
|
|
385
|
-
versionInternal = module.exports.getPackageVersion(registry);
|
|
386
|
-
} else {
|
|
387
|
-
versionInternal = JSON.parse(
|
|
388
|
-
fs.readFileSync(path.join(__dirname, "../../../package.json"))
|
|
389
|
-
).version;
|
|
390
|
-
}
|
|
391
|
-
if (versionInternal) {
|
|
392
|
-
console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`);
|
|
393
|
-
const ruleDictInternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionInternal);
|
|
394
|
-
module.exports.genDocFiles(ruleDictInternal, docsPath);
|
|
395
|
-
}
|
|
396
|
-
// Get rules released (external on npm)
|
|
397
|
-
const npmRegistry = "https://registry.npmjs.org";
|
|
398
|
-
let versionExternal;
|
|
399
|
-
if (!prepareRelease) {
|
|
400
|
-
versionExternal = module.exports.getPackageVersion(npmRegistry);
|
|
401
|
-
} else {
|
|
402
|
-
versionExternal = JSON.parse(
|
|
403
|
-
fs.readFileSync(path.join(__dirname, "../../../package.json"))
|
|
404
|
-
).version;
|
|
405
|
-
}
|
|
406
|
-
if (versionExternal) {
|
|
407
|
-
console.log(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`);
|
|
408
|
-
const ruleDictExternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionExternal, release);
|
|
409
|
-
module.exports.genDocFiles(ruleDictExternal, docsPath, release);
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
// Get "custom" rules
|
|
413
|
-
const ruleDict = module.exports.getRuleDict(docsPath, rulePath, testPath);
|
|
414
|
-
module.exports.genDocFiles(ruleDict, docsPath);
|
|
415
|
-
}
|
|
416
|
-
console.log('Done!')
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
getRuleDict: function (docsPath, rulePath, testPath, versionRequired='0.0.0', release=false) {
|
|
420
|
-
let mdRule, mdRuleSources, mdRuleContents;
|
|
421
|
-
let ruleDict = {};
|
|
422
|
-
fs.readdirSync(rulePath).filter(function (file) {
|
|
423
|
-
if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
|
|
424
|
-
const rule = path.basename(file).replace(path.extname(file), "");
|
|
425
|
-
if (rule !== "cds-compile-error") {
|
|
426
|
-
const ruleTestPath = path.join(testPath, rule, "rule.test.js");
|
|
427
|
-
|
|
428
|
-
// Get rule meta information
|
|
429
|
-
const ruleMeta = require(path.join(rulePath, file)).meta;
|
|
430
|
-
const version = ruleMeta.docs.version;
|
|
431
|
-
|
|
432
|
-
if ((release && semver.satisfies(version, `<=${versionRequired}`))
|
|
433
|
-
|| (!release)) {
|
|
434
|
-
const details = ruleMeta.docs.description;
|
|
435
|
-
const category = ruleMeta.docs.category;
|
|
436
|
-
const fixable = ruleMeta.fixable;
|
|
437
|
-
const messages = ruleMeta.messages;
|
|
438
|
-
const recommended = ruleMeta.docs.recommended;
|
|
439
|
-
const suggestions = ruleMeta.hasSuggestions;
|
|
440
|
-
|
|
441
|
-
let underConstruction = "";
|
|
442
|
-
if (!release && semver.satisfies(version, `>${versionRequired}`)) {
|
|
443
|
-
underConstruction = "🚧";
|
|
444
|
-
console.log(` > 🚧 Rule '${rule}' still under construction.\n`)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
let isFixable = "";
|
|
448
|
-
if (["code", "whitespace"].includes(fixable)) {
|
|
449
|
-
isFixable = "🔧";
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
let isRecommended = "";
|
|
453
|
-
if (recommended === true) {
|
|
454
|
-
isRecommended = "✔️";
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
let hasSuggestions = "";
|
|
458
|
-
if (suggestions === true) {
|
|
459
|
-
hasSuggestions = "💡";
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const ruleDictEntry = {
|
|
463
|
-
name: rule,
|
|
464
|
-
details,
|
|
465
|
-
recommended: isRecommended,
|
|
466
|
-
fixable: isFixable,
|
|
467
|
-
hasSuggestions,
|
|
468
|
-
construction: underConstruction,
|
|
469
|
-
messages,
|
|
470
|
-
version: version,
|
|
471
|
-
};
|
|
472
|
-
mdRule = module.exports.getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
|
|
473
|
-
mdRuleContents = "";
|
|
474
|
-
if (!release && underConstruction) {
|
|
475
|
-
mdRuleContents += `## ${rule}\n<span class='shifted'>${underConstruction} <span class='label'>${category}</span></span>\n\n`;
|
|
476
|
-
} else {
|
|
477
|
-
mdRuleContents += `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`;
|
|
478
|
-
}
|
|
479
|
-
mdRuleContents += `### Rule Details\n${details}\n\n`;
|
|
480
|
-
if (mdRule) {
|
|
481
|
-
mdRuleContents += `### Examples\n${mdRule}\n\n`;
|
|
482
|
-
}
|
|
483
|
-
mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
|
|
484
|
-
mdRuleSources = `### Resources\n[Rule & Documentation source](${path
|
|
485
|
-
.relative(docsPath, path.join(rulePath, `${rule}.js`))
|
|
486
|
-
.replace(/\\/g, "/")})\n\n`;
|
|
487
|
-
|
|
488
|
-
ruleDictEntry.contents = mdRuleContents;
|
|
489
|
-
ruleDictEntry.sources = mdRuleSources;
|
|
490
|
-
if (Object.keys(ruleDict).includes(category)) {
|
|
491
|
-
ruleDict[category].push(ruleDictEntry);
|
|
492
|
-
} else {
|
|
493
|
-
ruleDict[category] = [
|
|
494
|
-
{
|
|
495
|
-
name: rule,
|
|
496
|
-
details,
|
|
497
|
-
recommended: isRecommended,
|
|
498
|
-
fixable: isFixable,
|
|
499
|
-
hasSuggestions,
|
|
500
|
-
version: version,
|
|
501
|
-
contents: mdRuleContents,
|
|
502
|
-
sources: mdRuleSources,
|
|
503
|
-
construction: underConstruction
|
|
504
|
-
},
|
|
505
|
-
];
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
return ruleDict;
|
|
512
|
-
},
|
|
513
|
-
|
|
514
|
-
getRuleExamples: function (ruleTestPath, testPath, ruleDictEntry) {
|
|
515
|
-
// Get rule valid/invalid tests
|
|
516
|
-
let mdRule = "";
|
|
517
|
-
if (fs.existsSync(ruleTestPath)) {
|
|
518
|
-
const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
|
|
519
|
-
const filename = module.exports.getKeyFromTest(
|
|
520
|
-
ruleTest,
|
|
521
|
-
"filename"
|
|
522
|
-
);
|
|
523
|
-
let errorsString = module.exports.getKeyFromTest(
|
|
524
|
-
ruleTest,
|
|
525
|
-
"errors"
|
|
526
|
-
);
|
|
527
|
-
const re = /(\S+):/gm;
|
|
528
|
-
errorsString = errorsString
|
|
529
|
-
.replace(re, `"$&`)
|
|
530
|
-
.replace(/:/gm, '":')
|
|
531
|
-
.replace(/`/gm, '"');
|
|
532
|
-
const errors = JSONC.parse(`{${errorsString}}`).errors;
|
|
533
|
-
const valid = fs.readFileSync(
|
|
534
|
-
path.join(testPath, ruleDictEntry.name, "valid", filename),
|
|
535
|
-
"utf8"
|
|
536
|
-
);
|
|
537
|
-
let invalid = fs.readFileSync(
|
|
538
|
-
path.join(testPath, ruleDictEntry.name, "invalid", filename),
|
|
539
|
-
"utf8"
|
|
540
|
-
);
|
|
541
|
-
const insertAt = (str, sub, pos) =>
|
|
542
|
-
`${str.slice(0, pos)}${sub}${str.slice(pos)}`;
|
|
543
|
-
let errorsSorted = []
|
|
544
|
-
errors.forEach((err) => {
|
|
545
|
-
if (errorsSorted.length === 0) {
|
|
546
|
-
errorsSorted = [err];
|
|
547
|
-
} else {
|
|
548
|
-
const errLast = errorsSorted[errorsSorted.length - 1];
|
|
549
|
-
if (err.line > errLast.line) {
|
|
550
|
-
errorsSorted.push(err)
|
|
551
|
-
} else if (err.line < errLast.line) {
|
|
552
|
-
errorsSorted.unshift(err);
|
|
553
|
-
} else {
|
|
554
|
-
if (err.column > errLast.column) {
|
|
555
|
-
errorsSorted.push(err)
|
|
556
|
-
} else if (err.line < errLast.line) {
|
|
557
|
-
errorsSorted.unshift(err)
|
|
558
|
-
} else {
|
|
559
|
-
errorsSorted.push(err)
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
errorsSorted.reverse().forEach((err, i) => {
|
|
565
|
-
if (err.messageId) {
|
|
566
|
-
let msg = ruleDictEntry.messages[err.messageId];
|
|
567
|
-
let data;
|
|
568
|
-
if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
|
|
569
|
-
data = errorsSorted[i].suggestions[0].data;
|
|
570
|
-
}
|
|
571
|
-
if (data) {
|
|
572
|
-
Object.keys(data).forEach((d) => {
|
|
573
|
-
msg = msg.replace(`{{${d}}}`, data[d]);
|
|
574
|
-
})
|
|
575
|
-
}
|
|
576
|
-
err.message = msg;
|
|
577
|
-
}
|
|
578
|
-
const msg = err.message.replace(/"/gm, "`");
|
|
579
|
-
if (err.line) {
|
|
580
|
-
const code = invalid.split("\n");
|
|
581
|
-
code[err.line - 1] = insertAt(
|
|
582
|
-
code[err.line - 1],
|
|
583
|
-
"</span>",
|
|
584
|
-
err.endColumn - 1
|
|
585
|
-
);
|
|
586
|
-
code[err.line - 1] = insertAt(
|
|
587
|
-
code[err.line - 1],
|
|
588
|
-
`<span style="display:inline-block; position:relative; border-bottom:2pt dotted red" title="${msg}">`,
|
|
589
|
-
err.column - 1
|
|
590
|
-
);
|
|
591
|
-
invalid = code.join("\n");
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
mdRule +=
|
|
596
|
-
`<span>✔️ Example of ` +
|
|
597
|
-
`<span style="color:green">correct</span> ` +
|
|
598
|
-
`code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
|
|
599
|
-
mdRule +=
|
|
600
|
-
`<span>❌ Example of ` +
|
|
601
|
-
`<span style="color:red">incorrect</span> ` +
|
|
602
|
-
`code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
|
|
603
|
-
}
|
|
604
|
-
return mdRule;
|
|
605
|
-
},
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Gets all relevant rules information (dictionary of rules contents, lists
|
|
609
|
-
* of 'env' and 'model' rules) for the rules directory provided
|
|
610
|
-
* - Default categories is 'model'
|
|
611
|
-
* @param {string} dirname
|
|
612
|
-
* @returns rules information
|
|
613
|
-
*/
|
|
614
|
-
getRules(dirname) {
|
|
615
|
-
const rules = {};
|
|
616
|
-
const listEnvRules = [];
|
|
617
|
-
const listModelRules = [];
|
|
618
|
-
fs.readdirSync(dirname).forEach((file) => {
|
|
619
|
-
if (path.extname(file) === ".js" && file !== "index.js") {
|
|
620
|
-
const rulename = file.replace(".js", "");
|
|
621
|
-
let rule = require(path.join(dirname, file));
|
|
622
|
-
if (!rule.meta) {
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
rule = module.exports.applyRuleDefaults(rule);
|
|
626
|
-
rules[rulename] = rule;
|
|
627
|
-
const category =
|
|
628
|
-
rules[rulename].meta.docs.category || categories["model"];
|
|
629
|
-
if (
|
|
630
|
-
!listEnvRules.includes(rulename) &&
|
|
631
|
-
category === categories["env"]
|
|
632
|
-
) {
|
|
633
|
-
listEnvRules.push(rulename);
|
|
634
|
-
}
|
|
635
|
-
if (
|
|
636
|
-
!listModelRules.includes(rulename) &&
|
|
637
|
-
category === categories["model"]
|
|
638
|
-
) {
|
|
639
|
-
listModelRules.push(rulename);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
const listRules = listEnvRules.concat(listModelRules);
|
|
644
|
-
return { rules, listRules, listEnvRules, listModelRules };
|
|
645
|
-
},
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Sets defaults for rule meta data for missing required
|
|
649
|
-
* propeties:
|
|
650
|
-
* - Rule is of type "problem"
|
|
651
|
-
* - Rule is in model category
|
|
652
|
-
* - Rule severity is "error"
|
|
653
|
-
* @param {*} rule
|
|
654
|
-
* @returns
|
|
655
|
-
*/
|
|
656
|
-
applyRuleDefaults(rule) {
|
|
657
|
-
let ruleSanitized;
|
|
658
|
-
if (rule.meta) {
|
|
659
|
-
ruleSanitized = { ...rule };
|
|
660
|
-
if (!rule.meta.type) {
|
|
661
|
-
ruleSanitized.meta.type = "problem";
|
|
662
|
-
}
|
|
663
|
-
if (rule.meta.docs && !rule.meta.docs.category) {
|
|
664
|
-
ruleSanitized.meta.docs.category = categories["model"];
|
|
665
|
-
}
|
|
666
|
-
if (rule.meta.docs.recommended && !rule.meta.severity) {
|
|
667
|
-
ruleSanitized.meta.severity = "error";
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
return ruleSanitized;
|
|
671
|
-
},
|
|
672
|
-
|
|
673
|
-
populateRules: function (context, customRulesDir) {
|
|
674
|
-
const configPath = Cache.get("configpath") || "";
|
|
675
|
-
// Allow for custom rules
|
|
676
|
-
if (configPath) {
|
|
677
|
-
let customRulesPath = path.join(
|
|
678
|
-
Cache.get("configpath"),
|
|
679
|
-
customRulesDir,
|
|
680
|
-
"rules"
|
|
681
|
-
);
|
|
682
|
-
let customRulesInfo;
|
|
683
|
-
if (fs.existsSync(customRulesPath)) {
|
|
684
|
-
customRulesInfo = module.exports.getRules(customRulesPath);
|
|
685
|
-
Cache.set("rulesInfo", {
|
|
686
|
-
listEnvRules: context.listEnvRules.concat(
|
|
687
|
-
customRulesInfo.listEnvRules
|
|
688
|
-
),
|
|
689
|
-
listModelRules: context.listModelRules.concat(
|
|
690
|
-
customRulesInfo.listModelRules
|
|
691
|
-
),
|
|
692
|
-
listRules: context.listRules.concat(customRulesInfo.listRules),
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
},
|
|
697
|
-
};
|