@sap/eslint-plugin-cds 2.4.1 → 2.5.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 +16 -2
- package/lib/api/index.js +9 -8
- package/lib/conf/all.js +21 -0
- package/lib/conf/index.js +22 -0
- package/lib/conf/recommended.js +18 -0
- package/lib/constants.js +10 -8
- package/lib/index.js +11 -32
- package/lib/parser.js +158 -7
- package/lib/rules/assoc2many-ambiguous-key.js +21 -38
- package/lib/rules/auth-no-empty-restrictions.js +13 -21
- package/lib/rules/auth-use-requires.js +15 -15
- package/lib/rules/auth-valid-restrict-grant.js +71 -34
- package/lib/rules/auth-valid-restrict-keys.js +22 -16
- package/lib/rules/auth-valid-restrict-to.js +71 -27
- package/lib/rules/auth-valid-restrict-where.js +24 -15
- package/lib/rules/index.js +26 -0
- package/lib/rules/latest-cds-version.js +8 -7
- package/lib/rules/min-node-version.js +10 -9
- package/lib/rules/no-db-keywords.js +16 -15
- package/lib/rules/no-dollar-prefixed-names.js +9 -7
- package/lib/rules/no-join-on-draft-enabled-entities.js +9 -24
- package/lib/rules/require-2many-oncond.js +9 -14
- package/lib/rules/sql-cast-suggestion.js +6 -20
- package/lib/rules/start-elements-lowercase.js +5 -8
- package/lib/rules/start-entities-uppercase.js +8 -11
- package/lib/rules/valid-csv-header.js +66 -66
- package/lib/{api/lint.d.ts → types.d.ts} +4 -7
- package/lib/utils/Cache.js +33 -0
- package/lib/utils/Colors.js +9 -0
- package/lib/utils/createRule.js +304 -0
- package/lib/utils/createRuleDocs.js +361 -0
- package/lib/utils/{fuzzySearch.js → findFuzzy.js} +0 -2
- package/lib/utils/genDocs.js +363 -0
- package/lib/utils/getConfigPath.js +33 -0
- package/lib/utils/getConfiguredFileTypes.js +10 -0
- package/lib/utils/getFileExtensions.js +8 -0
- package/lib/utils/isConfiguredFileType.js +20 -0
- package/lib/utils/jsonc.js +1 -1
- package/lib/utils/jsoncParser.js +1 -0
- package/lib/utils/rules.js +112 -1041
- package/lib/utils/runRuleTester.js +111 -0
- package/package.json +4 -4
- package/lib/processor.js +0 -50
- package/lib/utils/helpers.js +0 -94
- package/lib/utils/model.js +0 -393
- package/lib/utils/ruleHelpers.js +0 -199
- package/lib/utils/ruleTester.js +0 -78
- package/lib/utils/validate.js +0 -36
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper for ESLint's Rule creator:
|
|
3
|
+
* https://eslint.org/docs/developer-guide/working-with-rules
|
|
4
|
+
* - Must follow the ESLint prescribed convention for all rule exports
|
|
5
|
+
* - ESLint uses 'create' function to traverse its AST nodes
|
|
6
|
+
* - Since we do not work with an AST for cds models, a dummy 'Programm' node is used as an entry point
|
|
7
|
+
* - More eslint-like API
|
|
8
|
+
* - More convenience for error reports
|
|
9
|
+
* @param {CDSRuleSpec} spec
|
|
10
|
+
* @returns {RuleModule}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { SourceCode } = require("eslint");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const Cache = require("./Cache");
|
|
16
|
+
const constants = require("../constants");
|
|
17
|
+
const isConfiguredFileType = require("./isConfiguredFileType");
|
|
18
|
+
const cds = require("@sap/cds");
|
|
19
|
+
const LOG = cds.debug("lint:plugin");
|
|
20
|
+
let filePrev = "";
|
|
21
|
+
|
|
22
|
+
const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
|
|
23
|
+
const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
|
|
24
|
+
|
|
25
|
+
module.exports = (spec) => {
|
|
26
|
+
let { meta, create } = spec;
|
|
27
|
+
meta = setMetaDefaults(meta);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
meta,
|
|
31
|
+
create: (context) => ({
|
|
32
|
+
Program: (node) => {
|
|
33
|
+
try {
|
|
34
|
+
const file = context.getFilename();
|
|
35
|
+
if (file !== filePrev) {
|
|
36
|
+
LOG && LOG(`File: ${context.getFilename()}`);
|
|
37
|
+
}
|
|
38
|
+
const cdscontext = extendContext(node, context, meta);
|
|
39
|
+
const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks } = checkEntryCriteria(meta, cdscontext);
|
|
40
|
+
|
|
41
|
+
switch (meta.model) {
|
|
42
|
+
case "none":
|
|
43
|
+
if (doEnvironmentChecks) {
|
|
44
|
+
if (isTest || !Cache.has(`rule:${cdscontext.id}`)) {
|
|
45
|
+
LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`);
|
|
46
|
+
Cache.set(`rule:${cdscontext.id}`, "done");
|
|
47
|
+
createReport(node, cdscontext, meta, create);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case "inferred":
|
|
53
|
+
if (isValidFile && doRootModelChecks) {
|
|
54
|
+
if (isTest || !Cache.has(`rule:${context.id}`)) {
|
|
55
|
+
LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`);
|
|
56
|
+
Cache.set(`rule:${cdscontext.id}`, "done");
|
|
57
|
+
createReport(node, cdscontext, meta, create);
|
|
58
|
+
} else {
|
|
59
|
+
if (Cache.has(`report:${context.getFilename()}:${context.id}`)) {
|
|
60
|
+
const reports = Cache.get(`report:${context.getFilename()}:${context.id}`);
|
|
61
|
+
for (const r of Array.from(reports)) {
|
|
62
|
+
context.report(r);
|
|
63
|
+
}
|
|
64
|
+
Cache.remove(`report:${context.getFilename()}:${context.id}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
default:
|
|
71
|
+
if (isValidFile) {
|
|
72
|
+
LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`);
|
|
73
|
+
createReport(node, cdscontext, meta, create);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
filePrev = file;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
LOG && LOG(err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function isRunningWithCDSLint() {
|
|
87
|
+
return process.argv[0].endsWith("node") && process.argv[1].endsWith("cds") && process.argv[2] === "lint";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkEntryCriteria(meta, cdscontext) {
|
|
91
|
+
const isTest = Cache.has("test");
|
|
92
|
+
cds.resolve.cache = {};
|
|
93
|
+
const roots = cds.resolve("*", { root: process.cwd() });
|
|
94
|
+
const isProjectRoot = roots && roots.length > 0 ? true : false;
|
|
95
|
+
const isValidFile = isConfiguredFileType(cdscontext.getFilename(), "FILES");
|
|
96
|
+
const isRunningWithCdsLint = isRunningWithCDSLint();
|
|
97
|
+
const doRootModelChecks = isTest || (isProjectRoot && (isRunningWithCdsLint || cdscontext.options.includes("show")));
|
|
98
|
+
// Also lint empty folders (i.e. lintText "" API)
|
|
99
|
+
const doEnvironmentChecks =
|
|
100
|
+
isTest || (isProjectRoot && isRunningWithCdsLint && cdscontext.getSourceCode().lines[0] === "");
|
|
101
|
+
return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function setMetaDefaults(meta) {
|
|
105
|
+
if (!meta.severity) meta.severity = constants.DEFAULT_RULE_SEVERITY;
|
|
106
|
+
if (meta.docs && !meta.docs.category) meta.docs.category = constants.DEFAULT_RULE_CATEGORY;
|
|
107
|
+
if (!meta.model) meta.model = "parsed";
|
|
108
|
+
return meta;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get report descriptors from created rules. These can take varios forms,
|
|
113
|
+
* from minimal return, up to fully defined ESLint report descriptors values,
|
|
114
|
+
* with or without visitor keys:
|
|
115
|
+
* - String is interpreted as the 'message' property
|
|
116
|
+
* - Object with known CDS Visitor keys and ESLint report descriptor values
|
|
117
|
+
* - Object with ESLint report dedscriptor keys/ values
|
|
118
|
+
* @param {*} cdscontext
|
|
119
|
+
* @param {*} create
|
|
120
|
+
* @returns
|
|
121
|
+
*/
|
|
122
|
+
function createReport(node, cdscontext, meta, create) {
|
|
123
|
+
const handlers = create(cdscontext);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* TODO: Can these be rewritten to have a visitor? Note, that so far,
|
|
127
|
+
* rules without a visitor cannot use eslint disable comments
|
|
128
|
+
* A rule have no visitors and just return a single check:
|
|
129
|
+
* - Model Validation rules which have no well-defined CSN entry point
|
|
130
|
+
* - Environment rules
|
|
131
|
+
*/
|
|
132
|
+
switch (typeof handlers) {
|
|
133
|
+
case "function":
|
|
134
|
+
handlers();
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case "object": {
|
|
138
|
+
if (meta.model !== "none") {
|
|
139
|
+
const model = cdscontext.getModel();
|
|
140
|
+
if (model) {
|
|
141
|
+
model.forall((d) => {
|
|
142
|
+
Object.entries(handlers)
|
|
143
|
+
.filter(([type, lazy]) => d.is(type))
|
|
144
|
+
.forEach(([lazy, handler]) => {
|
|
145
|
+
try {
|
|
146
|
+
handler(d);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extendContext(node, context, meta) {
|
|
160
|
+
if (!Cache.has("test") && !Cache.has("rootpath")) {
|
|
161
|
+
Cache.set("rootpath", path.resolve("."));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const reportWrapper = (r) => {
|
|
165
|
+
const line = r.loc ? r.loc.start.line : r.node.loc.start.line;
|
|
166
|
+
if (!isRuleDisabled(line, context)) {
|
|
167
|
+
|
|
168
|
+
if (meta.model === "inferred") {
|
|
169
|
+
const file = resolveFilePath(r.file);
|
|
170
|
+
if (cdscontext.getFilename() === file) {
|
|
171
|
+
delete r.file;
|
|
172
|
+
context.report(r);
|
|
173
|
+
} else {
|
|
174
|
+
delete r.file;
|
|
175
|
+
if (r.messageId) {
|
|
176
|
+
r.message = meta.messages[r.messageId];
|
|
177
|
+
delete r.message;
|
|
178
|
+
}
|
|
179
|
+
if (r) {
|
|
180
|
+
let reports = new Set();
|
|
181
|
+
if (Cache.has(`report:${file}:${context.id}`)) {
|
|
182
|
+
reports = Cache.get(`report:${file}:${context.id}`);
|
|
183
|
+
}
|
|
184
|
+
reports.add(r);
|
|
185
|
+
Cache.set(`report:${file}:${context.id}`, reports);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} else {
|
|
190
|
+
context.report(r);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const descriptors = Object.getOwnPropertyDescriptors(context);
|
|
196
|
+
descriptors.report = {
|
|
197
|
+
value: reportWrapper,
|
|
198
|
+
writable: false,
|
|
199
|
+
enumerable: true,
|
|
200
|
+
configurable: false
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors);
|
|
204
|
+
cdscontext.getModel =
|
|
205
|
+
meta.model === "inferred" ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn;
|
|
206
|
+
cdscontext.getEnvironment = () => {
|
|
207
|
+
const options = context.options;
|
|
208
|
+
return options && options[0] && options[0].environment ? options[0].environment : undefined;
|
|
209
|
+
};
|
|
210
|
+
cdscontext.getLocation = context.parserServices.getLocation;
|
|
211
|
+
cdscontext.getNode = Object.keys(context.parserServices).length > 0 ? context.parserServices.getNode : () => node;
|
|
212
|
+
return cdscontext;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isRuleDisabled(line, cdscontext) {
|
|
216
|
+
let isDisabled = false;
|
|
217
|
+
if (cdscontext) {
|
|
218
|
+
const sourcecode = cdscontext.getSourceCode();
|
|
219
|
+
const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line);
|
|
220
|
+
const id = cdscontext.id;
|
|
221
|
+
isDisabled = line && id in rulesDisabled && rulesDisabled[id] === "off";
|
|
222
|
+
}
|
|
223
|
+
return isDisabled;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getDisabled(code, sourcecode, line) {
|
|
227
|
+
const listDisabled = [];
|
|
228
|
+
let rules = Cache.get("rules");
|
|
229
|
+
const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: "on" }), {});
|
|
230
|
+
let matches = [];
|
|
231
|
+
if (code) {
|
|
232
|
+
matches = [...code.matchAll(REGEX_COMMENTS)];
|
|
233
|
+
if (matches.length > 0) {
|
|
234
|
+
matches.forEach((match) => {
|
|
235
|
+
if (match) {
|
|
236
|
+
const index = match.index;
|
|
237
|
+
match = match[0];
|
|
238
|
+
if (match.includes("*/")) {
|
|
239
|
+
match = match.split("*/")[0].replace("/*", "");
|
|
240
|
+
} else if (match.includes("//")) {
|
|
241
|
+
match = match.split("//")[1];
|
|
242
|
+
}
|
|
243
|
+
if (match) {
|
|
244
|
+
match = match.trim();
|
|
245
|
+
}
|
|
246
|
+
["disable", "enable"].forEach((keyword) => {
|
|
247
|
+
const loc = sourcecode.getLocFromIndex(index);
|
|
248
|
+
const disableType = match.split(" ")[0];
|
|
249
|
+
let disableRules = match.split(`${disableType} `)[1];
|
|
250
|
+
disableRules = disableRules
|
|
251
|
+
? disableRules.split(",").map((rule) => rule.trim())
|
|
252
|
+
: Object.keys(rules).map((rule) => `@sap/cds/${rule}`);
|
|
253
|
+
let comment = {};
|
|
254
|
+
if ([`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)) {
|
|
255
|
+
comment = disableType.includes("-next-line")
|
|
256
|
+
? {
|
|
257
|
+
lineComment: loc.line,
|
|
258
|
+
lineDisabled: loc.line + 1,
|
|
259
|
+
rules: disableRules,
|
|
260
|
+
type: keyword
|
|
261
|
+
}
|
|
262
|
+
: {
|
|
263
|
+
lineComment: loc.line,
|
|
264
|
+
lineDisabled: loc.line,
|
|
265
|
+
rules: disableRules,
|
|
266
|
+
type: keyword
|
|
267
|
+
};
|
|
268
|
+
if (!disableType.includes("-line")) {
|
|
269
|
+
comment.lineDisabled = "EOF";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
listDisabled.push(comment);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
for (const el of listDisabled.filter(
|
|
277
|
+
(d) => d.lineComment > line && (d.lineDisabled === "EOF" || d.lineDisabled === line)
|
|
278
|
+
)) {
|
|
279
|
+
if (el.lineDisabled === "EOF") {
|
|
280
|
+
el.lineDisabled = getLastLine(code);
|
|
281
|
+
}
|
|
282
|
+
if (el.rules) {
|
|
283
|
+
el.rules.forEach((rule) => {
|
|
284
|
+
if (el.type === "disable") {
|
|
285
|
+
rulesDisabled[rule] = "off";
|
|
286
|
+
} else if (el.type === "enable") {
|
|
287
|
+
rulesDisabled[rule] = "on";
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return rulesDisabled;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getLastLine(code) {
|
|
298
|
+
const lines = typeof code === "string" ? SourceCode.splitLines(code) : code;
|
|
299
|
+
return lines.length - 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveFilePath(file) {
|
|
303
|
+
return path.isAbsolute(file) ? file : path.join(Cache.get("rootpath"), file);
|
|
304
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates custom rules documentation (markdown files)
|
|
3
|
+
* for user according to contents of:
|
|
4
|
+
* - Rule files
|
|
5
|
+
* - Test files (with valid/invalid/fixed examples)
|
|
6
|
+
* @param {string} projectPath
|
|
7
|
+
* @param {string} customRulesDir
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const { exit } = require("process");
|
|
14
|
+
const cp = require("child_process");
|
|
15
|
+
const semver = require("semver");
|
|
16
|
+
|
|
17
|
+
const { mkdirp } = require("@sap/cds/lib/utils");
|
|
18
|
+
const JSONC = require("./jsoncParser");
|
|
19
|
+
const IS_WIN = os.platform() === "win32";
|
|
20
|
+
|
|
21
|
+
module.exports = async (projectPath, customRulesDir, registry, prepareRelease = false) => {
|
|
22
|
+
let docsPath, rulePath, testPath, release;
|
|
23
|
+
|
|
24
|
+
if (!projectPath) {
|
|
25
|
+
docsPath = path.join(__dirname, "../../docs");
|
|
26
|
+
rulePath = path.join(__dirname, "../rules");
|
|
27
|
+
testPath = path.join(__dirname, "../../test/rules");
|
|
28
|
+
release = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"))).version;
|
|
29
|
+
} else {
|
|
30
|
+
docsPath = path.join(projectPath, `${customRulesDir}/docs`);
|
|
31
|
+
rulePath = path.join(projectPath, `${customRulesDir}/rules`);
|
|
32
|
+
testPath = path.join(projectPath, `${customRulesDir}/tests`);
|
|
33
|
+
await Promise.all(
|
|
34
|
+
[docsPath, rulePath, testPath].filter((path) => !fs.existsSync(path)).map((path) => mkdirp(path))
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (registry) {
|
|
38
|
+
// Get rules (internal on artifactory)
|
|
39
|
+
const versionInternal = prepareRelease
|
|
40
|
+
? JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"))).version
|
|
41
|
+
: _getPackageVersion(registry);
|
|
42
|
+
if (versionInternal) {
|
|
43
|
+
console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`);
|
|
44
|
+
const ruleDictInternal = _getRuleDict(docsPath, rulePath, testPath, versionInternal);
|
|
45
|
+
_genDocFiles(ruleDictInternal, docsPath);
|
|
46
|
+
}
|
|
47
|
+
// Get rules released (external on npm)
|
|
48
|
+
const npmRegistry = "https://registry.npmjs.org";
|
|
49
|
+
const versionExternal = prepareRelease
|
|
50
|
+
? JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"))).version
|
|
51
|
+
: _getPackageVersion(npmRegistry);
|
|
52
|
+
if (versionExternal) {
|
|
53
|
+
console.log(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`);
|
|
54
|
+
const ruleDictExternal = _getRuleDict(docsPath, rulePath, testPath, versionExternal, release);
|
|
55
|
+
_genDocFiles(ruleDictExternal, docsPath, release);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Get "custom" rules
|
|
59
|
+
const ruleDict = _getRuleDict(docsPath, rulePath, testPath);
|
|
60
|
+
_genDocFiles(ruleDict, docsPath);
|
|
61
|
+
}
|
|
62
|
+
console.log("Done!");
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generates markdown documentation files for:
|
|
67
|
+
* - Overview of all rules in form of markdown table (RuleList)
|
|
68
|
+
* - List of all rules details in form of markdown page (Rules)
|
|
69
|
+
* If used internally within the @sap/eslint-plugin-cds, this
|
|
70
|
+
* also generates 'released' files, which only contain information
|
|
71
|
+
* on rules published until the currently released version.
|
|
72
|
+
* @param ruleDict
|
|
73
|
+
* @param docsPath
|
|
74
|
+
* @param release
|
|
75
|
+
*/
|
|
76
|
+
function _genDocFiles(ruleDict, docsPath, release = false) {
|
|
77
|
+
let suffix = "";
|
|
78
|
+
if (release) {
|
|
79
|
+
suffix = "-released";
|
|
80
|
+
}
|
|
81
|
+
const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`);
|
|
82
|
+
const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`);
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(ruleDocsPath)) {
|
|
85
|
+
fs.writeFileSync(ruleDocsPath, "", "utf8");
|
|
86
|
+
}
|
|
87
|
+
if (!fs.existsSync(ruleListDocsPath)) {
|
|
88
|
+
fs.writeFileSync(ruleListDocsPath, "", "utf8");
|
|
89
|
+
}
|
|
90
|
+
const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
|
|
91
|
+
const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
|
|
92
|
+
|
|
93
|
+
// Get rules table
|
|
94
|
+
const mdRuleList = _genMdRules(ruleDict, release, true);
|
|
95
|
+
|
|
96
|
+
// Get rule details
|
|
97
|
+
let mdRules = _genMdRules(ruleDict, release, false);
|
|
98
|
+
/* eslint-disable-next-line no-unused-vars */
|
|
99
|
+
Object.entries(ruleDict).forEach(([category, rules]) => {
|
|
100
|
+
rules.forEach(function (rule) {
|
|
101
|
+
mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
|
|
106
|
+
fs.writeFileSync(ruleDocsPath, mdRules, "utf8");
|
|
107
|
+
fs.writeFileSync(ruleListDocsPath, mdRuleList, "utf8");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _getPackageVersion(registry) {
|
|
112
|
+
let version;
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = cp
|
|
116
|
+
.execSync(`npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`, {
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
shell: IS_WIN,
|
|
119
|
+
stdio: "pipe",
|
|
120
|
+
})
|
|
121
|
+
.toString();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.err(`Failed to connect to ${registry} - check your connection and try again.`);
|
|
124
|
+
exit(0);
|
|
125
|
+
}
|
|
126
|
+
version = JSON.parse(result)["version"];
|
|
127
|
+
if (!version) {
|
|
128
|
+
console.err(`Failed to get latest plugin version from ${registry} - check your connection and try again.`);
|
|
129
|
+
exit(0);
|
|
130
|
+
}
|
|
131
|
+
return version;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generates overview table of all rules based on rule dictionary.
|
|
136
|
+
* @param ruleDict
|
|
137
|
+
* @param release
|
|
138
|
+
* @param table
|
|
139
|
+
* @returns Markdown table
|
|
140
|
+
*/
|
|
141
|
+
function _genMdRules(ruleDict, release, table = true) {
|
|
142
|
+
let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
|
|
143
|
+
if (table) {
|
|
144
|
+
mdRules += `Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n`;
|
|
145
|
+
mdRules += `✔️ if the plugin's "recommended" configuration enables the rule\n\n`;
|
|
146
|
+
mdRules += `🔧 if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`;
|
|
147
|
+
mdRules += `💡 if problems reported by the rule are manually fixable (editor)\n\n`;
|
|
148
|
+
if (!release) {
|
|
149
|
+
mdRules += `🚧 if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`;
|
|
150
|
+
mdRules += "| | | | | | | |\n";
|
|
151
|
+
mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
|
|
152
|
+
} else {
|
|
153
|
+
mdRules += "| | | | | | | |\n";
|
|
154
|
+
mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
|
|
155
|
+
}
|
|
156
|
+
/* eslint-disable-next-line no-unused-vars */
|
|
157
|
+
Object.entries(ruleDict).forEach(([, rules]) => {
|
|
158
|
+
rules.forEach(function (rule) {
|
|
159
|
+
mdRules += release
|
|
160
|
+
? `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | | | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`
|
|
161
|
+
: `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | ${rule.construction} | | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
mdRules += "\n";
|
|
165
|
+
}
|
|
166
|
+
return mdRules;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _getRuleDict(docsPath, rulePath, testPath, versionRequired = "0.0.0", release = false) {
|
|
170
|
+
let mdRule, mdRuleSources, mdRuleContents;
|
|
171
|
+
const ruleDict = {};
|
|
172
|
+
fs.readdirSync(rulePath).filter((file) => {
|
|
173
|
+
if (path.extname(file).toLowerCase() === ".js" && file !== "index.js") {
|
|
174
|
+
const rule = path.basename(file).replace(path.extname(file), "");
|
|
175
|
+
const ruleTestPath = path.join(testPath, rule, "rule.test.js");
|
|
176
|
+
|
|
177
|
+
// Get rule meta information
|
|
178
|
+
const ruleMeta = require(path.join(rulePath, file)).meta;
|
|
179
|
+
const version = ruleMeta.docs.version;
|
|
180
|
+
|
|
181
|
+
if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
|
|
182
|
+
const details = ruleMeta.docs.description;
|
|
183
|
+
const category = ruleMeta.docs.category;
|
|
184
|
+
const fixable = ruleMeta.fixable;
|
|
185
|
+
const messages = ruleMeta.messages;
|
|
186
|
+
const recommended = ruleMeta.docs.recommended;
|
|
187
|
+
const suggestions = ruleMeta.hasSuggestions;
|
|
188
|
+
|
|
189
|
+
let underConstruction = "";
|
|
190
|
+
if (!release && (version === "TBD" || semver.satisfies(version, `>${versionRequired}`))) {
|
|
191
|
+
underConstruction = "🚧";
|
|
192
|
+
console.log(` > 🚧 Rule '${rule}' still under construction.\n`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const isFixable = ["code", "whitespace"].includes(fixable) ? "🔧" : "";
|
|
196
|
+
const isRecommended = recommended === true ? "✔️" : "";
|
|
197
|
+
const hasSuggestions = suggestions === true ? "💡" : "";
|
|
198
|
+
|
|
199
|
+
const ruleDictEntry = {
|
|
200
|
+
name: rule,
|
|
201
|
+
details,
|
|
202
|
+
recommended: isRecommended,
|
|
203
|
+
fixable: isFixable,
|
|
204
|
+
hasSuggestions,
|
|
205
|
+
construction: underConstruction,
|
|
206
|
+
messages,
|
|
207
|
+
version: version,
|
|
208
|
+
};
|
|
209
|
+
mdRule = _getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
|
|
210
|
+
mdRuleContents = "";
|
|
211
|
+
|
|
212
|
+
mdRuleContents +=
|
|
213
|
+
!release && underConstruction
|
|
214
|
+
? `## ${rule}\n<span class='shifted'>${underConstruction} <span class='label'>${category}</span></span>\n\n`
|
|
215
|
+
: `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`;
|
|
216
|
+
|
|
217
|
+
mdRuleContents += `### Rule Details\n${details}\n\n`;
|
|
218
|
+
if (mdRule) {
|
|
219
|
+
mdRuleContents += `### Examples\n${mdRule}\n\n`;
|
|
220
|
+
}
|
|
221
|
+
mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
|
|
222
|
+
mdRuleSources = `### Resources\n[Rule & Documentation source](${path
|
|
223
|
+
.relative(docsPath, path.join(rulePath, `${rule}.js`))
|
|
224
|
+
.replace(/\\/g, "/")})\n\n`;
|
|
225
|
+
|
|
226
|
+
ruleDictEntry.contents = mdRuleContents;
|
|
227
|
+
ruleDictEntry.sources = mdRuleSources;
|
|
228
|
+
if (Object.keys(ruleDict).includes(category)) {
|
|
229
|
+
ruleDict[category].push(ruleDictEntry);
|
|
230
|
+
} else {
|
|
231
|
+
ruleDict[category] = [
|
|
232
|
+
{
|
|
233
|
+
name: rule,
|
|
234
|
+
details,
|
|
235
|
+
recommended: isRecommended,
|
|
236
|
+
fixable: isFixable,
|
|
237
|
+
hasSuggestions,
|
|
238
|
+
version: version,
|
|
239
|
+
contents: mdRuleContents,
|
|
240
|
+
sources: mdRuleSources,
|
|
241
|
+
construction: underConstruction,
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return ruleDict;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Gets value for a given key in allowed keys for input of runRuleTester api
|
|
253
|
+
* @param {string} text test input for ruleTester
|
|
254
|
+
* @param {string} key key to get value for
|
|
255
|
+
* @returns Value for given key
|
|
256
|
+
*/
|
|
257
|
+
function _getKeyFromTest(text, key) {
|
|
258
|
+
let result = "";
|
|
259
|
+
if (["root", "rule", "filename", "parser"].includes(key)) {
|
|
260
|
+
const regexTestKey = new RegExp(`${key}:.*$`, "gm");
|
|
261
|
+
const matchTestKey = regexTestKey.exec(text);
|
|
262
|
+
if (matchTestKey) {
|
|
263
|
+
const quote = matchTestKey[0].replace(",", "").slice(-1);
|
|
264
|
+
const regexTestValue = new RegExp(`${quote}[\\s\\S]*?(\\${quote},?)`, "gm");
|
|
265
|
+
const matchValue = regexTestValue.exec(matchTestKey[0]);
|
|
266
|
+
if (matchValue) {
|
|
267
|
+
const regex = new RegExp(`${quote},`, "gm");
|
|
268
|
+
const regex2 = new RegExp(`${quote}`, "gm");
|
|
269
|
+
result = matchValue[0].replace(regex, "").replace(regex2, "");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else if (key === "errors") {
|
|
273
|
+
const regexTestKey = new RegExp(`${key}:.*$(([\\s]+.+)+])?`, "gm");
|
|
274
|
+
const matchTestKey = regexTestKey.exec(text);
|
|
275
|
+
if (matchTestKey) {
|
|
276
|
+
result = matchTestKey[0];
|
|
277
|
+
}
|
|
278
|
+
} else if (key === "data") {
|
|
279
|
+
const regexTestKey = new RegExp(`${key}:.*}`, "gm");
|
|
280
|
+
const matchTestKey = regexTestKey.exec(text);
|
|
281
|
+
if (matchTestKey) {
|
|
282
|
+
result = matchTestKey[0];
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
result = `No parameter \\'${key}\\' found in ruleTest`;
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _getRuleExamples(ruleTestPath, testPath, ruleDictEntry) {
|
|
291
|
+
// Get rule valid/invalid tests
|
|
292
|
+
let mdRule = "";
|
|
293
|
+
if (fs.existsSync(ruleTestPath)) {
|
|
294
|
+
const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
|
|
295
|
+
const filename = _getKeyFromTest(ruleTest, "filename");
|
|
296
|
+
let errorsString = _getKeyFromTest(ruleTest, "errors");
|
|
297
|
+
const re = /(\S+):/gm;
|
|
298
|
+
errorsString = errorsString.replace(re, `"$&`).replace(/:/gm, '":').replace(/`/gm, '"');
|
|
299
|
+
const errors = JSONC.parse(`{${errorsString}}`).errors;
|
|
300
|
+
const valid = fs.readFileSync(path.join(testPath, ruleDictEntry.name, "valid", filename), "utf8");
|
|
301
|
+
let invalid = fs.readFileSync(path.join(testPath, ruleDictEntry.name, "invalid", filename), "utf8");
|
|
302
|
+
const insertAt = (str, sub, pos) => `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
|
|
303
|
+
let errorsSorted = [];
|
|
304
|
+
errors.forEach((err) => {
|
|
305
|
+
if (errorsSorted.length === 0) {
|
|
306
|
+
errorsSorted = [err];
|
|
307
|
+
} else {
|
|
308
|
+
const errLast = errorsSorted[errorsSorted.length - 1];
|
|
309
|
+
if (err.line > errLast.line) {
|
|
310
|
+
errorsSorted.push(err);
|
|
311
|
+
} else if (err.line < errLast.line) {
|
|
312
|
+
errorsSorted.unshift(err);
|
|
313
|
+
} else {
|
|
314
|
+
if (err.column > errLast.column) {
|
|
315
|
+
errorsSorted.push(err);
|
|
316
|
+
} else if (err.line < errLast.line) {
|
|
317
|
+
errorsSorted.unshift(err);
|
|
318
|
+
} else {
|
|
319
|
+
errorsSorted.push(err);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
errorsSorted.reverse().forEach((err, i) => {
|
|
325
|
+
if (err.messageId) {
|
|
326
|
+
let msg = ruleDictEntry.messages[err.messageId];
|
|
327
|
+
let data;
|
|
328
|
+
if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
|
|
329
|
+
data = errorsSorted[i].suggestions[0].data;
|
|
330
|
+
}
|
|
331
|
+
if (data) {
|
|
332
|
+
Object.keys(data).forEach((d) => {
|
|
333
|
+
msg = msg.replace(`{{${d}}}`, data[d]);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
err.message = msg;
|
|
337
|
+
}
|
|
338
|
+
const msg = err.message.replace(/"/gm, "`");
|
|
339
|
+
if (err.line) {
|
|
340
|
+
const code = invalid.split("\n");
|
|
341
|
+
code[err.line - 1] = insertAt(code[err.line - 1], "</i></b></span>", err.endColumn - 1);
|
|
342
|
+
code[err.line - 1] = insertAt(
|
|
343
|
+
code[err.line - 1],
|
|
344
|
+
`<span style="display:inline-block; position:relative; color:red; border-bottom:2pt dotted red" title="${msg}"><b><i>`,
|
|
345
|
+
err.column - 1
|
|
346
|
+
);
|
|
347
|
+
invalid = code.join("\n");
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
mdRule +=
|
|
352
|
+
`<span>✔️ Example of ` +
|
|
353
|
+
`<span style="color:green">correct</span> ` +
|
|
354
|
+
`code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
|
|
355
|
+
mdRule +=
|
|
356
|
+
`<span>❌ Example of ` +
|
|
357
|
+
`<span style="color:red">incorrect</span> ` +
|
|
358
|
+
`code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
|
|
359
|
+
}
|
|
360
|
+
return mdRule;
|
|
361
|
+
}
|
|
@@ -56,7 +56,6 @@ module.exports = (input, list, log, keepCase=false) => {
|
|
|
56
56
|
return minDistWords.sort();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
59
|
const levDistance = (a, b) => {
|
|
61
60
|
|
|
62
61
|
if (cache[a] && cache[a][b]) {
|
|
@@ -86,7 +85,6 @@ const levDistance = (a, b) => {
|
|
|
86
85
|
return addToCache(a, b, levDist);
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
|
|
90
88
|
const addToCache = (a, b, value) => {
|
|
91
89
|
cache[a] = cache[a] || {};
|
|
92
90
|
cache[a][b] = value;
|