@sap/eslint-plugin-cds 2.3.4 → 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 +40 -0
- 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 +37 -0
- package/lib/rules/auth-use-requires.js +38 -0
- package/lib/rules/auth-valid-restrict-grant.js +105 -0
- package/lib/rules/auth-valid-restrict-keys.js +43 -0
- package/lib/rules/auth-valid-restrict-to.js +130 -0
- package/lib/rules/auth-valid-restrict-where.js +89 -0
- 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 -17
- package/lib/rules/no-dollar-prefixed-names.js +7 -33
- 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 +7 -10
- package/lib/rules/start-entities-uppercase.js +6 -9
- 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} +9 -4
- 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 +85 -945
- package/lib/utils/runRuleTester.js +111 -0
- package/package.json +8 -5
- package/lib/processor.js +0 -47
- package/lib/utils/helpers.js +0 -47
- package/lib/utils/model.js +0 -387
- package/lib/utils/ruleHelpers.js +0 -56
- package/lib/utils/ruleTester.js +0 -79
- package/lib/utils/validate.js +0 -36
package/lib/utils/rules.js
CHANGED
|
@@ -1,973 +1,113 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
const SEP = "[,;\t]";
|
|
2
|
+
const EOL = "\\r?\\n";
|
|
4
3
|
|
|
5
|
-
const
|
|
6
|
-
const os = require("os");
|
|
7
|
-
const cp = require("child_process");
|
|
8
|
-
const cds = require("@sap/cds");
|
|
9
|
-
|
|
10
|
-
const semver = require("semver");
|
|
11
|
-
const path = require("path");
|
|
12
|
-
const { mkdirp } = require("@sap/cds/lib/utils");
|
|
13
|
-
const { SourceCode } = require("eslint");
|
|
14
|
-
|
|
15
|
-
const { hasDebugFlag, isValidFile, isVSCodeEditor } = require("./helpers");
|
|
16
|
-
const { DEFAULT_RULE_CATEGORY, DEFAULT_RULE_SEVERITY, DEFAULT_RULE_TYPE } = require("../constants");
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
Cache,
|
|
20
|
-
getAST,
|
|
21
|
-
getLastLine,
|
|
22
|
-
initRootModel,
|
|
23
|
-
updateModel,
|
|
24
|
-
compileModelFromFile,
|
|
25
|
-
isNewConfigPath,
|
|
26
|
-
isFileInModel,
|
|
27
|
-
hasFileChanged,
|
|
28
|
-
getLocation,
|
|
29
|
-
getRange,
|
|
30
|
-
loadConfigPath,
|
|
31
|
-
} = require("./model");
|
|
32
|
-
|
|
33
|
-
const { isValidModel } = require("./validate");
|
|
34
|
-
const { exit } = require("process");
|
|
35
|
-
|
|
36
|
-
const JSONC = require("./jsonc");
|
|
37
|
-
const IS_WIN = os.platform() === "win32";
|
|
38
|
-
const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
|
|
39
|
-
const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
|
|
40
|
-
|
|
41
|
-
function doReport(cdscontext, reportDescriptor, d) {
|
|
42
|
-
const retouchLocations = (descriptor) => {
|
|
43
|
-
if (d) {
|
|
44
|
-
descriptor.loc = descriptor.loc || cdscontext.cds.getLocation(d.name, d);
|
|
45
|
-
descriptor.file = descriptor.file || (d.$location ? d.$location.file : "unknown.cds");
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
switch (typeof reportDescriptor) {
|
|
50
|
-
case "string":
|
|
51
|
-
reportDescriptor = { message: reportDescriptor };
|
|
52
|
-
retouchLocations(reportDescriptor);
|
|
53
|
-
cdscontext.report(reportDescriptor);
|
|
54
|
-
break;
|
|
55
|
-
case "object":
|
|
56
|
-
if (!Array.isArray(reportDescriptor)) {
|
|
57
|
-
retouchLocations(reportDescriptor);
|
|
58
|
-
cdscontext.report(reportDescriptor);
|
|
59
|
-
} else {
|
|
60
|
-
reportDescriptor.forEach((x) => {
|
|
61
|
-
if (typeof x === "string") {
|
|
62
|
-
x = { message: x };
|
|
63
|
-
}
|
|
64
|
-
retouchLocations(x);
|
|
65
|
-
cdscontext.report(x);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function reportCompilationErr(meta, node, cdscontext, err) {
|
|
73
|
-
const lint = { err: true };
|
|
74
|
-
const name = err.constructor.name;
|
|
75
|
-
if (err.messages) {
|
|
76
|
-
lint.message = `${name}: ${err.message}`;
|
|
77
|
-
// const text = err.message.split(/CDS Compilation failed\s+/, "");
|
|
78
|
-
err.messages.forEach((msg) => {
|
|
79
|
-
if (msg.severity === "Error") {
|
|
80
|
-
meta.severity = 2;
|
|
81
|
-
lint.file = msg.location.file;
|
|
82
|
-
lint.message = `${name}: ${msg}`;
|
|
83
|
-
lint.filePath = cdscontext.filePath;
|
|
84
|
-
return {
|
|
85
|
-
meta,
|
|
86
|
-
create: cdscontext.report(lint),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function reportErr(meta, node, cdscontext, err) {
|
|
94
|
-
const lint = { err: true };
|
|
95
|
-
lint.err = true;
|
|
96
|
-
if (hasDebugFlag() && !isVSCodeEditor()) {
|
|
97
|
-
lint.message = err.stack;
|
|
98
|
-
meta.severity = 2;
|
|
99
|
-
return {
|
|
100
|
-
meta,
|
|
101
|
-
create: cdscontext.report(lint),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Expands CDS context object with some CDS properties
|
|
108
|
-
* We also retrieve the file contents cached by the preprocessor
|
|
109
|
-
* @param {RuleContext} context
|
|
110
|
-
* @param {RuleNode} node
|
|
111
|
-
* @returns cdscontext
|
|
112
|
-
*/
|
|
113
|
-
function addCDSContext(context, node) {
|
|
114
|
-
const filePath = (context.filePath = context.getPhysicalFilename());
|
|
115
|
-
const configPath = !Cache.has("test") ? loadConfigPath(filePath) : path.dirname(filePath);
|
|
116
|
-
let sourcecode = context.getSourceCode();
|
|
117
|
-
const code = sourcecode.getText(node) || Cache.get(`file:${filePath}`);
|
|
118
|
-
if (code) {
|
|
119
|
-
sourcecode = new SourceCode(code, getAST(code));
|
|
120
|
-
}
|
|
121
|
-
const options = context.options;
|
|
122
|
-
const id = context.id;
|
|
123
|
-
return {
|
|
124
|
-
_context: context,
|
|
125
|
-
report: module.exports.reportProxy(context.report),
|
|
126
|
-
cds: module.exports.cdsProxy(cds, { code, filePath, configPath, id, options }),
|
|
127
|
-
id,
|
|
128
|
-
code,
|
|
129
|
-
sourcecode,
|
|
130
|
-
options,
|
|
131
|
-
filePath,
|
|
132
|
-
configPath,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Wrapper for ESLint's Rule creator:
|
|
138
|
-
* https://eslint.org/docs/developer-guide/working-with-rules
|
|
139
|
-
* - Must follow the ESLint prescribed convention for all rule exports
|
|
140
|
-
* - ESLint uses 'create' function to traverse its AST nodes
|
|
141
|
-
* - Since we do not work with an AST for cds models, a dummy 'Programm'
|
|
142
|
-
* node is used as an entry point
|
|
143
|
-
* - For all ESLint rules, we have two entry points for additional checks:
|
|
144
|
-
* 1. Before ESLint's report via context.report()
|
|
145
|
-
* (see getProxyReport())
|
|
146
|
-
* - More eslint-like API
|
|
147
|
-
* - More convenience re error reports
|
|
148
|
-
* @param {CDSRuleSpec} spec
|
|
149
|
-
* @returns {RuleModule}
|
|
150
|
-
*/
|
|
151
|
-
function createRule(spec) {
|
|
152
|
-
const { meta, create } = spec;
|
|
153
|
-
|
|
154
|
-
if (!meta.type) meta.type = DEFAULT_RULE_TYPE;
|
|
155
|
-
if (!meta.severity) meta.severity = DEFAULT_RULE_SEVERITY;
|
|
156
|
-
if (meta.docs && !meta.docs.category) meta.docs.category = DEFAULT_RULE_CATEGORY;
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
meta,
|
|
160
|
-
create: (context) => ({
|
|
161
|
-
Program: function (node) {
|
|
162
|
-
const cdscontext = addCDSContext(context, node, meta);
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const { report, ...ruleDescriptors } = cdscontext;
|
|
166
|
-
const handlers = create({ ...ruleDescriptors, report: (r) => report(r) });
|
|
167
|
-
|
|
168
|
-
// Report descriptors with fake visitor 'all'
|
|
169
|
-
// Used for environment rules and rules which require another compiled model
|
|
170
|
-
// (i.e. sql or odata)
|
|
171
|
-
if (handlers.all) {
|
|
172
|
-
let reportDescriptor = handlers.all();
|
|
173
|
-
doReport(cdscontext, reportDescriptor);
|
|
174
|
-
} else {
|
|
175
|
-
// TODO: Use external address
|
|
176
|
-
// Report descriptors with visitors using using `any.is()`
|
|
177
|
-
// https://pages.github.tools.sap/cap/docs/node.js/cds-reflect
|
|
178
|
-
if (cdscontext.cds.model) {
|
|
179
|
-
cdscontext.cds.model.forall((d) =>
|
|
180
|
-
Object.entries(handlers)
|
|
181
|
-
.filter(([type, lazy]) => d.is(type))
|
|
182
|
-
.forEach(([lazy, handler]) => doReport(cdscontext, handler(d), d))
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch (err) {
|
|
187
|
-
// Report errors in ESLint style
|
|
188
|
-
if (err.messages) {
|
|
189
|
-
// Always show model compile errors
|
|
190
|
-
reportCompilationErr(meta, node, cdscontext, err);
|
|
191
|
-
} else {
|
|
192
|
-
// Thrown errors are only shown on console with --debug
|
|
193
|
-
reportErr(meta, node, cdscontext, err);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
}),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Checks whether a lint rule has been disabled by eslint-disable
|
|
203
|
-
* comments at a given location
|
|
204
|
-
* @param entry lint report
|
|
205
|
-
* @param cdscontext cds context object
|
|
206
|
-
* @param rules all availabe rules
|
|
207
|
-
* @returns boolean
|
|
208
|
-
*/
|
|
209
|
-
|
|
210
|
-
function isRuleDisabled(entry, cdscontext) {
|
|
211
|
-
let isDisabled = false;
|
|
212
|
-
if (entry.loc && entry.loc.start) {
|
|
213
|
-
const line = entry.loc.start.line;
|
|
214
|
-
if (cdscontext) {
|
|
215
|
-
const rulesDisabled = _getDisabled(cdscontext.code, cdscontext.sourcecode, line);
|
|
216
|
-
const id = cdscontext.id;
|
|
217
|
-
isDisabled = line && id in rulesDisabled && rulesDisabled[id] === "off";
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return isDisabled;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Turns rules "on" or "off" for given line according to eslint-disable
|
|
225
|
-
* comments:
|
|
226
|
-
* 1. Reads code string and extracts a list of comments (in order)
|
|
227
|
-
* 2. Initiates rulesDisabled array with all rules "on" by default
|
|
228
|
-
* 3. Switches rules "off" (or "on" again) based on disable comment
|
|
229
|
-
* @param code current code
|
|
230
|
-
* @param sourcecode source code object to get index from
|
|
231
|
-
* @param line current code line to analyze
|
|
232
|
-
* @returns rules dictionary with rules being either 'on' and 'off'
|
|
233
|
-
*/
|
|
234
|
-
function _getDisabled(code, sourcecode, line) {
|
|
235
|
-
const listDisabled = [];
|
|
236
|
-
let { listEnvRules, listModelRules, listRules } = Cache.get("rulesInfo");
|
|
237
|
-
const rulesDisabled = listRules.reduce((o, key) => ({ ...o, [key]: "on" }), {});
|
|
238
|
-
let matches = [];
|
|
239
|
-
if (code) {
|
|
240
|
-
matches = [...code.matchAll(REGEX_COMMENTS)];
|
|
241
|
-
if (matches.length > 0) {
|
|
242
|
-
matches.forEach((match) => {
|
|
243
|
-
if (match) {
|
|
244
|
-
const index = match.index;
|
|
245
|
-
match = match[0];
|
|
246
|
-
if (match.includes("*/")) {
|
|
247
|
-
match = match.split("*/")[0].replace("/*", "");
|
|
248
|
-
} else if (match.includes("//")) {
|
|
249
|
-
match = match.split("//")[1];
|
|
250
|
-
}
|
|
251
|
-
if (match) {
|
|
252
|
-
match = match.trim();
|
|
253
|
-
}
|
|
254
|
-
["disable", "enable"].forEach((keyword) => {
|
|
255
|
-
const loc = sourcecode.getLocFromIndex(index);
|
|
256
|
-
const disableType = match.split(" ")[0];
|
|
257
|
-
let disableRules = match.split(`${disableType} `)[1];
|
|
258
|
-
disableRules = disableRules
|
|
259
|
-
? disableRules.split(",").map((rule) => rule.trim())
|
|
260
|
-
: listEnvRules.concat(listModelRules).map((rule) => `@sap/cds/${rule}`);
|
|
261
|
-
let comment = {};
|
|
262
|
-
if ([`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)) {
|
|
263
|
-
comment = disableType.includes("-next-line")
|
|
264
|
-
? {
|
|
265
|
-
lineComment: loc.line,
|
|
266
|
-
lineDisabled: loc.line + 1,
|
|
267
|
-
rules: disableRules,
|
|
268
|
-
type: keyword,
|
|
269
|
-
}
|
|
270
|
-
: {
|
|
271
|
-
lineComment: loc.line,
|
|
272
|
-
lineDisabled: loc.line,
|
|
273
|
-
rules: disableRules,
|
|
274
|
-
type: keyword,
|
|
275
|
-
};
|
|
276
|
-
if (!disableType.includes("-line")) {
|
|
277
|
-
comment.lineDisabled = "EOF";
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
listDisabled.push(comment);
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
for (const el of listDisabled.filter(
|
|
285
|
-
(d) => d.lineComment > line && (d.lineDisabled === "EOF" || d.lineDisabled === line)
|
|
286
|
-
)) {
|
|
287
|
-
if (el.lineDisabled === "EOF") {
|
|
288
|
-
el.lineDisabled = getLastLine(code);
|
|
289
|
-
}
|
|
290
|
-
if (el.rules) {
|
|
291
|
-
el.rules.forEach((rule) => {
|
|
292
|
-
if (el.type === "disable") {
|
|
293
|
-
rulesDisabled[rule] = "off";
|
|
294
|
-
} else if (el.type === "enable") {
|
|
295
|
-
rulesDisabled[rule] = "on";
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return rulesDisabled;
|
|
303
|
-
}
|
|
4
|
+
const findFuzzy = require("./findFuzzy");
|
|
304
5
|
|
|
305
6
|
module.exports = {
|
|
7
|
+
findFuzzy,
|
|
306
8
|
/**
|
|
307
|
-
*
|
|
308
|
-
* @param {
|
|
309
|
-
* @param {string} key key to get value for
|
|
310
|
-
* @returns Value for given key
|
|
9
|
+
*
|
|
10
|
+
* @param {*} e
|
|
311
11
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const quote = matchQuote[0].slice(-1);
|
|
317
|
-
const exprStart = `${key}:[\\s]+\\${quote}`;
|
|
318
|
-
const exprEnd = `(\\${quote},,?)`;
|
|
319
|
-
const regexKey = new RegExp(`${exprStart}[\\s\\S]*?${exprEnd}`, "gm");
|
|
320
|
-
const matchKey = regexKey.exec(text);
|
|
321
|
-
if (matchKey) {
|
|
322
|
-
const regexStart = new RegExp(`${exprStart}`, "gm");
|
|
323
|
-
const regexEnd = new RegExp(`${exprEnd}`, "gm");
|
|
324
|
-
return matchKey[0]
|
|
325
|
-
.replace(regexStart, "")
|
|
326
|
-
.replace(regexEnd, "")
|
|
327
|
-
.replace("fixable:", "")
|
|
328
|
-
.replace(/\\/gm, "")
|
|
329
|
-
.trim();
|
|
330
|
-
} else {
|
|
331
|
-
return "";
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
const regexBoolean = new RegExp(`${key}:[\\s]+true[\\s]?,`, "gm");
|
|
335
|
-
const matchBoolean = regexBoolean.exec(text);
|
|
336
|
-
if (matchBoolean) {
|
|
337
|
-
if (matchBoolean[0].includes("true,")) {
|
|
338
|
-
return true;
|
|
339
|
-
} else {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return "";
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
getPackageVersion: function (registry) {
|
|
348
|
-
let version;
|
|
349
|
-
let result;
|
|
350
|
-
try {
|
|
351
|
-
result = cp
|
|
352
|
-
.execSync(`npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`, {
|
|
353
|
-
cwd: process.cwd(),
|
|
354
|
-
shell: IS_WIN,
|
|
355
|
-
stdio: "pipe",
|
|
356
|
-
})
|
|
357
|
-
.toString();
|
|
358
|
-
} catch (err) {
|
|
359
|
-
console.err(`Failed to connect to ${registry} - check your connection and try again.`);
|
|
360
|
-
exit(0);
|
|
361
|
-
}
|
|
362
|
-
version = JSON.parse(result)["version"];
|
|
363
|
-
if (!version) {
|
|
364
|
-
console.err(`Failed to get latest plugin version from ${registry} - check your connection and try again.`);
|
|
365
|
-
exit(0);
|
|
366
|
-
}
|
|
367
|
-
return version;
|
|
368
|
-
},
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Gets value for a given key in allowed keys for input of runRuleTester api
|
|
372
|
-
* @param {string} text test input for ruleTester
|
|
373
|
-
* @param {string} key key to get value for
|
|
374
|
-
* @returns Value for given key
|
|
375
|
-
*/
|
|
376
|
-
getKeyFromTest: function (text, key) {
|
|
377
|
-
let result = "";
|
|
378
|
-
if (["root", "rule", "filename", "parser"].includes(key)) {
|
|
379
|
-
const regexTestKey = new RegExp(`${key}:.*$`, "gm");
|
|
380
|
-
const matchTestKey = regexTestKey.exec(text);
|
|
381
|
-
if (matchTestKey) {
|
|
382
|
-
const quote = matchTestKey[0].replace(",", "").slice(-1);
|
|
383
|
-
const regexTestValue = new RegExp(`${quote}[\\s\\S]*?(\\${quote},?)`, "gm");
|
|
384
|
-
const matchValue = regexTestValue.exec(matchTestKey[0]);
|
|
385
|
-
if (matchValue) {
|
|
386
|
-
const regex = new RegExp(`${quote},`, "gm");
|
|
387
|
-
const regex2 = new RegExp(`${quote}`, "gm");
|
|
388
|
-
result = matchValue[0].replace(regex, "").replace(regex2, "");
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
} else if (key === "errors") {
|
|
392
|
-
const regexTestKey = new RegExp(`${key}:.*$(([\\s]+.+)+])?`, "gm");
|
|
393
|
-
const matchTestKey = regexTestKey.exec(text);
|
|
394
|
-
if (matchTestKey) {
|
|
395
|
-
result = matchTestKey[0];
|
|
396
|
-
}
|
|
397
|
-
} else if (key === "data") {
|
|
398
|
-
const regexTestKey = new RegExp(`${key}:.*}`, "gm");
|
|
399
|
-
const matchTestKey = regexTestKey.exec(text);
|
|
400
|
-
if (matchTestKey) {
|
|
401
|
-
result = matchTestKey[0];
|
|
402
|
-
}
|
|
403
|
-
} else {
|
|
404
|
-
result = `No parameter \\'${key}\\' found in ruleTest`;
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
407
|
-
},
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Generates overview table of all rules based on rule dictionary.
|
|
411
|
-
* @param ruleDict
|
|
412
|
-
* @param release
|
|
413
|
-
* @param table
|
|
414
|
-
* @returns Markdown table
|
|
415
|
-
*/
|
|
416
|
-
genMdRules: function (ruleDict, release, table = true) {
|
|
417
|
-
let mdRules = `# @sap/eslint-plugin-cds [latest]\n\n`;
|
|
418
|
-
if (table) {
|
|
419
|
-
mdRules += `Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n`;
|
|
420
|
-
mdRules += `✔️ if the plugin's "recommended" configuration enables the rule\n\n`;
|
|
421
|
-
mdRules += `🔧 if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`;
|
|
422
|
-
mdRules += `💡 if problems reported by the rule are manually fixable (editor)\n\n`;
|
|
423
|
-
if (!release) {
|
|
424
|
-
mdRules += `🚧 if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`;
|
|
425
|
-
mdRules += "| | | | | | | |\n";
|
|
426
|
-
mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
|
|
427
|
-
} else {
|
|
428
|
-
mdRules += "| | | | | | | |\n";
|
|
429
|
-
mdRules += "|:-:|:-:|:-:|:-:|-:|:-|:-|\n";
|
|
430
|
-
}
|
|
431
|
-
/* eslint-disable-next-line no-unused-vars */
|
|
432
|
-
Object.entries(ruleDict).forEach(([, rules]) => {
|
|
433
|
-
rules.forEach(function (rule) {
|
|
434
|
-
mdRules += release
|
|
435
|
-
? `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | | | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`
|
|
436
|
-
: `| ${rule.recommended} | ${rule.fixable} | ${rule.hasSuggestions} | ${rule.construction} | | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`;
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
mdRules += "\n";
|
|
440
|
-
}
|
|
441
|
-
return mdRules;
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Generates markdown documentation files for:
|
|
446
|
-
* - Overview of all rules in form of markdown table (RuleList)
|
|
447
|
-
* - List of all rules details in form of markdown page (Rules)
|
|
448
|
-
* If used internally within the @sap/eslint-plugin-cds, this
|
|
449
|
-
* also generates 'released' files, which only contain information
|
|
450
|
-
* on rules published until the currently released version.
|
|
451
|
-
* @param ruleDict
|
|
452
|
-
* @param docsPath
|
|
453
|
-
* @param release
|
|
454
|
-
*/
|
|
455
|
-
genDocFiles: function (ruleDict, docsPath, release = false) {
|
|
12
|
+
splitEntityName: function (e) {
|
|
13
|
+
// Entity names from CSN are of the form:
|
|
14
|
+
// <namespace>.<service>.<entity>.<'texts'|'localized'>|<composition value>
|
|
15
|
+
let prefix = "";
|
|
456
16
|
let suffix = "";
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`);
|
|
461
|
-
const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`);
|
|
462
|
-
|
|
463
|
-
if (!fs.existsSync(ruleDocsPath)) {
|
|
464
|
-
fs.writeFileSync(ruleDocsPath, "", "utf8");
|
|
465
|
-
}
|
|
466
|
-
if (!fs.existsSync(ruleListDocsPath)) {
|
|
467
|
-
fs.writeFileSync(ruleListDocsPath, "", "utf8");
|
|
468
|
-
}
|
|
469
|
-
const mdRulesCur = fs.readFileSync(ruleDocsPath, "utf8");
|
|
470
|
-
const mdRuleListCur = fs.readFileSync(ruleListDocsPath, "utf8");
|
|
471
|
-
|
|
472
|
-
// Get rules table
|
|
473
|
-
const mdRuleList = module.exports.genMdRules(ruleDict, release, true);
|
|
474
|
-
|
|
475
|
-
// Get rule details
|
|
476
|
-
let mdRules = module.exports.genMdRules(ruleDict, release, false);
|
|
477
|
-
/* eslint-disable-next-line no-unused-vars */
|
|
478
|
-
Object.entries(ruleDict).forEach(([category, rules]) => {
|
|
479
|
-
rules.forEach(function (rule) {
|
|
480
|
-
mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`;
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
|
|
485
|
-
fs.writeFileSync(ruleDocsPath, mdRules, "utf8");
|
|
486
|
-
fs.writeFileSync(ruleListDocsPath, mdRuleList, "utf8");
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Generates custom rules documentation (markdown files)
|
|
492
|
-
* for user according to contents of:
|
|
493
|
-
* - Rule files
|
|
494
|
-
* - Test files (with valid/invalid/fixed examples)
|
|
495
|
-
* @param {string} projectPath
|
|
496
|
-
* @param {string} customRulesDir
|
|
497
|
-
*/
|
|
498
|
-
async genDocs(projectPath, customRulesDir, registry, prepareRelease = false) {
|
|
499
|
-
let docsPath, rulePath, testPath, release;
|
|
17
|
+
let entityName = e.name;
|
|
18
|
+
const names = entityName.split(".");
|
|
19
|
+
entityName = names[names.length - 1];
|
|
500
20
|
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
} else {
|
|
507
|
-
docsPath = path.join(projectPath, `${customRulesDir}/docs`);
|
|
508
|
-
rulePath = path.join(projectPath, `${customRulesDir}/rules`);
|
|
509
|
-
testPath = path.join(projectPath, `${customRulesDir}/tests`);
|
|
510
|
-
await Promise.all(
|
|
511
|
-
[docsPath, rulePath, testPath].filter((path) => !fs.existsSync(path)).map((path) => mkdirp(path))
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (registry) {
|
|
516
|
-
// Get rules (internal on artifactory)
|
|
517
|
-
const versionInternal = prepareRelease
|
|
518
|
-
? JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"))).version
|
|
519
|
-
: module.exports.getPackageVersion(registry);
|
|
520
|
-
if (versionInternal) {
|
|
521
|
-
console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`);
|
|
522
|
-
const ruleDictInternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionInternal);
|
|
523
|
-
module.exports.genDocFiles(ruleDictInternal, docsPath);
|
|
21
|
+
if (entityName) {
|
|
22
|
+
// Managed composition get compiler tag `_up`
|
|
23
|
+
let isManagedComposition = false;
|
|
24
|
+
if (e.elements) {
|
|
25
|
+
isManagedComposition = Object.keys(e.elements).some((k) => k === "up_");
|
|
524
26
|
}
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const ruleDictExternal = module.exports.getRuleDict(docsPath, rulePath, testPath, versionExternal, release);
|
|
533
|
-
module.exports.genDocFiles(ruleDictExternal, docsPath, release);
|
|
27
|
+
// Check for compiler tags
|
|
28
|
+
let compilerTagsToExclude = ["texts", "localized"];
|
|
29
|
+
const isCompilerTag = compilerTagsToExclude.includes(entityName);
|
|
30
|
+
|
|
31
|
+
if (isManagedComposition || isCompilerTag) {
|
|
32
|
+
suffix = names[names.length - 1];
|
|
33
|
+
entityName = names[names.length - 2];
|
|
534
34
|
}
|
|
535
|
-
|
|
536
|
-
// Get "custom" rules
|
|
537
|
-
const ruleDict = module.exports.getRuleDict(docsPath, rulePath, testPath);
|
|
538
|
-
module.exports.genDocFiles(ruleDict, docsPath);
|
|
35
|
+
prefix = e.name.split(`.${entityName}`)[0];
|
|
539
36
|
}
|
|
540
|
-
|
|
37
|
+
return { prefix, entity: entityName, suffix };
|
|
541
38
|
},
|
|
542
39
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
|
|
556
|
-
const details = ruleMeta.docs.description;
|
|
557
|
-
const category = ruleMeta.docs.category;
|
|
558
|
-
const fixable = ruleMeta.fixable;
|
|
559
|
-
const messages = ruleMeta.messages;
|
|
560
|
-
const recommended = ruleMeta.docs.recommended;
|
|
561
|
-
const suggestions = ruleMeta.hasSuggestions;
|
|
562
|
-
|
|
563
|
-
let underConstruction = "";
|
|
564
|
-
if (!release && (version === "TBD" || semver.satisfies(version, `>${versionRequired}`))) {
|
|
565
|
-
underConstruction = "🚧";
|
|
566
|
-
console.log(` > 🚧 Rule '${rule}' still under construction.\n`);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const isFixable = ["code", "whitespace"].includes(fixable) ? "🔧" : "";
|
|
570
|
-
const isRecommended = recommended === true ? "✔️" : "";
|
|
571
|
-
const hasSuggestions = suggestions === true ? "💡" : "";
|
|
572
|
-
|
|
573
|
-
const ruleDictEntry = {
|
|
574
|
-
name: rule,
|
|
575
|
-
details,
|
|
576
|
-
recommended: isRecommended,
|
|
577
|
-
fixable: isFixable,
|
|
578
|
-
hasSuggestions,
|
|
579
|
-
construction: underConstruction,
|
|
580
|
-
messages,
|
|
581
|
-
version: version,
|
|
582
|
-
};
|
|
583
|
-
mdRule = module.exports.getRuleExamples(ruleTestPath, testPath, ruleDictEntry);
|
|
584
|
-
mdRuleContents = "";
|
|
585
|
-
|
|
586
|
-
mdRuleContents +=
|
|
587
|
-
!release && underConstruction
|
|
588
|
-
? `## ${rule}\n<span class='shifted'>${underConstruction} <span class='label'>${category}</span></span>\n\n`
|
|
589
|
-
: `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`;
|
|
590
|
-
|
|
591
|
-
mdRuleContents += `### Rule Details\n${details}\n\n`;
|
|
592
|
-
if (mdRule) {
|
|
593
|
-
mdRuleContents += `### Examples\n${mdRule}\n\n`;
|
|
594
|
-
}
|
|
595
|
-
mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`;
|
|
596
|
-
mdRuleSources = `### Resources\n[Rule & Documentation source](${path
|
|
597
|
-
.relative(docsPath, path.join(rulePath, `${rule}.js`))
|
|
598
|
-
.replace(/\\/g, "/")})\n\n`;
|
|
599
|
-
|
|
600
|
-
ruleDictEntry.contents = mdRuleContents;
|
|
601
|
-
ruleDictEntry.sources = mdRuleSources;
|
|
602
|
-
if (Object.keys(ruleDict).includes(category)) {
|
|
603
|
-
ruleDict[category].push(ruleDictEntry);
|
|
604
|
-
} else {
|
|
605
|
-
ruleDict[category] = [
|
|
606
|
-
{
|
|
607
|
-
name: rule,
|
|
608
|
-
details,
|
|
609
|
-
recommended: isRecommended,
|
|
610
|
-
fixable: isFixable,
|
|
611
|
-
hasSuggestions,
|
|
612
|
-
version: version,
|
|
613
|
-
contents: mdRuleContents,
|
|
614
|
-
sources: mdRuleSources,
|
|
615
|
-
construction: underConstruction,
|
|
616
|
-
},
|
|
617
|
-
];
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
return ruleDict;
|
|
40
|
+
_findInCode: function (miss, code) {
|
|
41
|
+
// middle
|
|
42
|
+
let match = new RegExp(SEP + miss + SEP).exec(code);
|
|
43
|
+
if (match) return match.index + 1;
|
|
44
|
+
// end of line
|
|
45
|
+
match = new RegExp(SEP + miss + EOL).exec(code);
|
|
46
|
+
if (match) return match.index + 1;
|
|
47
|
+
// start of doc
|
|
48
|
+
match = new RegExp("^" + miss + SEP).exec(code);
|
|
49
|
+
if (match) return match.index;
|
|
50
|
+
// somewhere (fallback)
|
|
51
|
+
return code.indexOf(miss);
|
|
623
52
|
},
|
|
624
53
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (fs.existsSync(ruleTestPath)) {
|
|
629
|
-
const ruleTest = fs.readFileSync(ruleTestPath, "utf8");
|
|
630
|
-
const filename = module.exports.getKeyFromTest(ruleTest, "filename");
|
|
631
|
-
let errorsString = module.exports.getKeyFromTest(ruleTest, "errors");
|
|
632
|
-
const re = /(\S+):/gm;
|
|
633
|
-
errorsString = errorsString.replace(re, `"$&`).replace(/:/gm, '":').replace(/`/gm, '"');
|
|
634
|
-
const errors = JSONC.parse(`{${errorsString}}`).errors;
|
|
635
|
-
const valid = fs.readFileSync(path.join(testPath, ruleDictEntry.name, "valid", filename), "utf8");
|
|
636
|
-
let invalid = fs.readFileSync(path.join(testPath, ruleDictEntry.name, "invalid", filename), "utf8");
|
|
637
|
-
const insertAt = (str, sub, pos) => `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
|
|
638
|
-
let errorsSorted = [];
|
|
639
|
-
errors.forEach((err) => {
|
|
640
|
-
if (errorsSorted.length === 0) {
|
|
641
|
-
errorsSorted = [err];
|
|
642
|
-
} else {
|
|
643
|
-
const errLast = errorsSorted[errorsSorted.length - 1];
|
|
644
|
-
if (err.line > errLast.line) {
|
|
645
|
-
errorsSorted.push(err);
|
|
646
|
-
} else if (err.line < errLast.line) {
|
|
647
|
-
errorsSorted.unshift(err);
|
|
648
|
-
} else {
|
|
649
|
-
if (err.column > errLast.column) {
|
|
650
|
-
errorsSorted.push(err);
|
|
651
|
-
} else if (err.line < errLast.line) {
|
|
652
|
-
errorsSorted.unshift(err);
|
|
653
|
-
} else {
|
|
654
|
-
errorsSorted.push(err);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
errorsSorted.reverse().forEach((err, i) => {
|
|
660
|
-
if (err.messageId) {
|
|
661
|
-
let msg = ruleDictEntry.messages[err.messageId];
|
|
662
|
-
let data;
|
|
663
|
-
if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
|
|
664
|
-
data = errorsSorted[i].suggestions[0].data;
|
|
665
|
-
}
|
|
666
|
-
if (data) {
|
|
667
|
-
Object.keys(data).forEach((d) => {
|
|
668
|
-
msg = msg.replace(`{{${d}}}`, data[d]);
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
err.message = msg;
|
|
672
|
-
}
|
|
673
|
-
const msg = err.message.replace(/"/gm, "`");
|
|
674
|
-
if (err.line) {
|
|
675
|
-
const code = invalid.split("\n");
|
|
676
|
-
code[err.line - 1] = insertAt(code[err.line - 1], "</i></b></span>", err.endColumn - 1);
|
|
677
|
-
code[err.line - 1] = insertAt(
|
|
678
|
-
code[err.line - 1],
|
|
679
|
-
`<span style="display:inline-block; position:relative; color:red; border-bottom:2pt dotted red" title="${msg}"><b><i>`,
|
|
680
|
-
err.column - 1
|
|
681
|
-
);
|
|
682
|
-
invalid = code.join("\n");
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
mdRule +=
|
|
687
|
-
`<span>✔️ Example of ` +
|
|
688
|
-
`<span style="color:green">correct</span> ` +
|
|
689
|
-
`code for this rule:</span>\n\n<pre><code>\n${valid}\n</code></pre>\n\n`;
|
|
690
|
-
mdRule +=
|
|
691
|
-
`<span>❌ Example of ` +
|
|
692
|
-
`<span style="color:red">incorrect</span> ` +
|
|
693
|
-
`code for this rule:</span>\n\n<pre><code>\n${invalid}\n</code></pre>`;
|
|
54
|
+
isEmptyString: function (value) {
|
|
55
|
+
if (typeof value !== "string" || (typeof value === "string" && value && value.length > 0)) {
|
|
56
|
+
return false;
|
|
694
57
|
}
|
|
695
|
-
return
|
|
58
|
+
return true;
|
|
696
59
|
},
|
|
697
60
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
* @returns rule object with sources and rule lists
|
|
703
|
-
*/
|
|
704
|
-
getRules: function (dirname, rulename) {
|
|
705
|
-
let rulesInfo;
|
|
706
|
-
if (Cache.has("rulesInfo")) {
|
|
707
|
-
rulesInfo = Cache.get("rulesInfo");
|
|
708
|
-
} else {
|
|
709
|
-
const rules = {};
|
|
710
|
-
const listEnvRules = [];
|
|
711
|
-
const listModelRules = [];
|
|
712
|
-
|
|
713
|
-
if (rulename) {
|
|
714
|
-
const file = `${rulename}.js`;
|
|
715
|
-
|
|
716
|
-
let rule = createRule(require(path.join(dirname, file)));
|
|
717
|
-
rules[rulename] = rule;
|
|
718
|
-
|
|
719
|
-
if (!listEnvRules.includes(rulename) && !isValidModel(file, "model")) {
|
|
720
|
-
listEnvRules.push(rulename);
|
|
721
|
-
}
|
|
722
|
-
if (!listModelRules.includes(rulename) && isValidModel(file, "model")) {
|
|
723
|
-
listModelRules.push(rulename);
|
|
724
|
-
}
|
|
725
|
-
} else {
|
|
726
|
-
fs.readdirSync(dirname).forEach((file) => {
|
|
727
|
-
if (path.extname(file) === ".js") {
|
|
728
|
-
const rulename = file.replace(".js", "");
|
|
729
|
-
|
|
730
|
-
let rule = createRule(require(path.join(dirname, file)));
|
|
731
|
-
rules[rulename] = rule;
|
|
732
|
-
|
|
733
|
-
if (!listEnvRules.includes(rulename) && !isValidModel(file, "model")) {
|
|
734
|
-
listEnvRules.push(rulename);
|
|
735
|
-
}
|
|
736
|
-
if (!listModelRules.includes(rulename) && isValidModel(file, "model")) {
|
|
737
|
-
listModelRules.push(rulename);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
});
|
|
61
|
+
isEmptyObject: function (value) {
|
|
62
|
+
function isEmpty(object) {
|
|
63
|
+
for (const property in object) {
|
|
64
|
+
return false;
|
|
741
65
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const recommended = Object.assign(
|
|
745
|
-
{},
|
|
746
|
-
...Object.entries(rules)
|
|
747
|
-
.filter(([, v]) => v.meta.docs.recommended)
|
|
748
|
-
.map(([k, v]) => ({ [`@sap/cds/${k}`]: v.meta.severity }))
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
const all = Object.assign({}, ...Object.entries(rules).map(([k, v]) => ({ [`@sap/cds/${k}`]: v.meta.severity })));
|
|
752
|
-
rulesInfo = { sources: rules, all, recommended, listRules, listEnvRules, listModelRules };
|
|
753
|
-
Cache.set("rulesInfo", rulesInfo);
|
|
66
|
+
return true;
|
|
754
67
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
// populateRules: function (context, customRulesDir) {
|
|
759
|
-
// const configPath = Cache.get("configpath") || "";
|
|
760
|
-
// // Allow for custom rules
|
|
761
|
-
// if (configPath) {
|
|
762
|
-
// let customRulesPath = path.join(Cache.get("configpath"), customRulesDir, "rules");
|
|
763
|
-
// let customRulesInfo;
|
|
764
|
-
// if (fs.existsSync(customRulesPath)) {
|
|
765
|
-
// customRulesInfo = module.exports.getRules(customRulesPath);
|
|
766
|
-
// Cache.set("rulesInfo", {
|
|
767
|
-
// listEnvRules: context.listEnvRules.concat(customRulesInfo.listEnvRules),
|
|
768
|
-
// listModelRules: context.listModelRules.concat(customRulesInfo.listModelRules),
|
|
769
|
-
// listRules: context.listRules.concat(customRulesInfo.listRules),
|
|
770
|
-
// });
|
|
771
|
-
// }
|
|
772
|
-
// }
|
|
773
|
-
// },
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Generates proxy for `@sap/cds` object which adds:
|
|
777
|
-
* - Extra properties (model, environment, etc.)
|
|
778
|
-
* - Option to cache function calls (apply)
|
|
779
|
-
* @param {cds} object
|
|
780
|
-
* @returns Proxy for cds
|
|
781
|
-
*/
|
|
782
|
-
cdsProxy: function (cds, { code, filePath, configPath, id, options }) {
|
|
783
|
-
const handler = {
|
|
784
|
-
get(target, prop, receiver) {
|
|
785
|
-
let value;
|
|
786
|
-
switch (prop) {
|
|
787
|
-
case "model":
|
|
788
|
-
value = module.exports.getModel(code, filePath, configPath, id, cds);
|
|
789
|
-
break;
|
|
790
|
-
case "environment":
|
|
791
|
-
value = module.exports.getEnvironment(options);
|
|
792
|
-
break;
|
|
793
|
-
case "getLocation":
|
|
794
|
-
value = getLocation;
|
|
795
|
-
break;
|
|
796
|
-
case "getRange":
|
|
797
|
-
value = getRange;
|
|
798
|
-
break;
|
|
799
|
-
default:
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
if (value) {
|
|
803
|
-
return value;
|
|
804
|
-
} else {
|
|
805
|
-
value = Reflect.get(target, prop, receiver);
|
|
806
|
-
if (value && typeof value == "object") {
|
|
807
|
-
value = new Proxy(value, handler);
|
|
808
|
-
}
|
|
809
|
-
return value;
|
|
810
|
-
}
|
|
811
|
-
},
|
|
812
|
-
apply(target, thisArg, argumentsList) {
|
|
813
|
-
// NOTE: Possible to add caching for expensive function calls
|
|
814
|
-
// here as the rule set grows further
|
|
815
|
-
const result = Reflect.apply(target, this, argumentsList);
|
|
816
|
-
return result;
|
|
817
|
-
},
|
|
818
|
-
};
|
|
819
|
-
return new Proxy(cds, handler);
|
|
820
|
-
},
|
|
821
|
-
/**
|
|
822
|
-
* Generates proxy for ESLint's context object which adds caching
|
|
823
|
-
* @param {CDSRuleReport} ESLint's context object
|
|
824
|
-
* @returns {Rule.ReportDescriptor}
|
|
825
|
-
*/
|
|
826
|
-
reportProxy: function (ruleReport) {
|
|
827
|
-
const handler = {
|
|
828
|
-
get(target, prop, receiver) {
|
|
829
|
-
const value = Reflect.get(target, prop, receiver);
|
|
830
|
-
if (typeof value !== "object") {
|
|
831
|
-
return value;
|
|
832
|
-
}
|
|
833
|
-
if (value) {
|
|
834
|
-
return new Proxy(value, handler);
|
|
835
|
-
}
|
|
836
|
-
return {
|
|
837
|
-
err: `Property ${prop} prop does not exist on object ${ruleReport}!`,
|
|
838
|
-
};
|
|
839
|
-
},
|
|
840
|
-
apply(target, thisArg, argumentsList) {
|
|
841
|
-
let report = false;
|
|
842
|
-
if (argumentsList.length > 0) {
|
|
843
|
-
argumentsList.forEach((lint) => {
|
|
844
|
-
if (lint) {
|
|
845
|
-
// Do not consider disabled content
|
|
846
|
-
if (!isRuleDisabled(lint, thisArg)) {
|
|
847
|
-
const isModelLint = lint.file && isValidFile(lint.file, "MODEL_FILES") && !lint.err;
|
|
848
|
-
if (isModelLint && module.exports.isDedicatedFile(lint, thisArg)) {
|
|
849
|
-
if (isVSCodeEditor()) {
|
|
850
|
-
report = true;
|
|
851
|
-
} else {
|
|
852
|
-
if (module.exports.isReportUnique(lint, "modelReports")) {
|
|
853
|
-
report = true;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (!lint.loc) {
|
|
858
|
-
lint.loc = module.exports.addDefaultLoc();
|
|
859
|
-
}
|
|
860
|
-
if (
|
|
861
|
-
!isModelLint &&
|
|
862
|
-
!lint.err &&
|
|
863
|
-
module.exports.isReportUnique(lint, "envReports", thisArg.configPath)
|
|
864
|
-
) {
|
|
865
|
-
report = true;
|
|
866
|
-
}
|
|
867
|
-
if (lint.err && (lint.file || module.exports.isReportUnique(lint, "errReports", thisArg.configPath))) {
|
|
868
|
-
report = true;
|
|
869
|
-
}
|
|
870
|
-
if (report) {
|
|
871
|
-
return thisArg._context.report(lint);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
},
|
|
878
|
-
};
|
|
879
|
-
return new Proxy(ruleReport, handler);
|
|
880
|
-
},
|
|
881
|
-
|
|
882
|
-
resolveFilePath: (file) => (path.isAbsolute(file) ? file : path.join(Cache.get("configpath"), file)),
|
|
883
|
-
|
|
884
|
-
isDedicatedFile: (lint, thisArg) =>
|
|
885
|
-
module.exports.resolveFilePath(lint.file) === thisArg.filePath || lint.file === "<stdin>.cds",
|
|
886
|
-
|
|
887
|
-
isReportUnique: function (lint, name, uniqueness) {
|
|
888
|
-
let report = false;
|
|
889
|
-
if (!uniqueness) {
|
|
890
|
-
uniqueness = JSON.stringify(lint);
|
|
891
|
-
}
|
|
892
|
-
if (!Cache.has(name) && uniqueness) {
|
|
893
|
-
const lintString = `${uniqueness}:${JSON.stringify(lint)}`;
|
|
894
|
-
Cache.set(name, [lintString]);
|
|
895
|
-
report = true;
|
|
896
|
-
} else {
|
|
897
|
-
if (uniqueness) {
|
|
898
|
-
const lintMessages = Cache.has(name) ? Cache.get(name) : [];
|
|
899
|
-
const lintString = `${uniqueness}:${JSON.stringify(lint)}`;
|
|
900
|
-
if (!lintMessages.includes(lintString)) {
|
|
901
|
-
lintMessages.push(lintString);
|
|
902
|
-
Cache.set(name, lintMessages);
|
|
903
|
-
report = true;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
68
|
+
if (typeof value !== "object" || (typeof value === "object" && !isEmpty(value)) ||
|
|
69
|
+
(typeof value === "object" && value && value.length > 0)) {
|
|
70
|
+
return false;
|
|
906
71
|
}
|
|
907
|
-
return
|
|
72
|
+
return true;
|
|
908
73
|
},
|
|
909
74
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
getModel: function (code, filePath, configPath) {
|
|
916
|
-
let model;
|
|
917
|
-
|
|
918
|
-
if (Cache.has("test")) {
|
|
919
|
-
return Cache.get(`model:${configPath}`);
|
|
75
|
+
isStringInArray(str, arr, caps=false) {
|
|
76
|
+
const notIncluded = !arr.includes(str);
|
|
77
|
+
if (!caps && notIncluded) {
|
|
78
|
+
return false;
|
|
920
79
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const isValidPluginFile = isValidFile(filePath, "FILES");
|
|
924
|
-
|
|
925
|
-
if (isValidPluginFile) {
|
|
926
|
-
model = initRootModel(configPath);
|
|
927
|
-
if (module.exports.isParkedModelFile(filePath, configPath)) {
|
|
928
|
-
return compileModelFromFile(code, filePath);
|
|
929
|
-
} else if (model) {
|
|
930
|
-
return model;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
} else {
|
|
934
|
-
const isValidModelFile = isValidFile(filePath, "MODEL_FILES");
|
|
935
|
-
|
|
936
|
-
if (module.exports.isParkedModelFile(filePath, configPath)) {
|
|
937
|
-
return compileModelFromFile(code, filePath);
|
|
938
|
-
} else if (isValidModelFile) {
|
|
939
|
-
const hasRootChanged = isNewConfigPath(configPath);
|
|
940
|
-
const fileChanged = hasFileChanged(code, filePath, configPath);
|
|
941
|
-
|
|
942
|
-
if (hasRootChanged || fileChanged) {
|
|
943
|
-
if (hasRootChanged) {
|
|
944
|
-
Cache.remove("envReports");
|
|
945
|
-
}
|
|
946
|
-
Cache.remove("modelReports");
|
|
947
|
-
Cache.remove("errReports");
|
|
948
|
-
model = updateModel(code, filePath, configPath);
|
|
949
|
-
} else {
|
|
950
|
-
model = Cache.get(`model:${configPath}`);
|
|
951
|
-
}
|
|
952
|
-
} else {
|
|
953
|
-
model = Cache.get(`model:${configPath}`);
|
|
954
|
-
}
|
|
955
|
-
return model;
|
|
80
|
+
if (caps && notIncluded && !arr.includes(str.toLowerCase()) && !arr.includes(str.toUpperCase())) {
|
|
81
|
+
return false;
|
|
956
82
|
}
|
|
83
|
+
return true;
|
|
957
84
|
},
|
|
958
85
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
86
|
+
getReplacementsSuggestions: function (context, value, loc) {
|
|
87
|
+
let invalid;
|
|
88
|
+
const lineToReplace = context.sourcecode.lines[loc.line];
|
|
89
|
+
var regExp = /\[([^)]+)\]/;
|
|
90
|
+
var matches = regExp.exec(lineToReplace);
|
|
91
|
+
if (matches && matches[0]) {
|
|
92
|
+
invalid = matches[0];
|
|
93
|
+
}
|
|
94
|
+
const startIndex = lineToReplace.indexOf(invalid);
|
|
95
|
+
const candidates = `['${value}']`;
|
|
96
|
+
const suggest = {
|
|
97
|
+
messageId: "ReplaceItemWith",
|
|
98
|
+
data: { invalid, candidates },
|
|
99
|
+
fix: (fixer) => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates),
|
|
100
|
+
};
|
|
101
|
+
return ({
|
|
102
|
+
messageId: "InvalidItem",
|
|
103
|
+
data: { invalid, candidates },
|
|
104
|
+
loc: {
|
|
105
|
+
start: { line: loc.line + 1, column: startIndex },
|
|
106
|
+
end: { line: loc.line + 1, column: startIndex + invalid.length },
|
|
107
|
+
},
|
|
108
|
+
file: loc.file,
|
|
109
|
+
suggest,
|
|
110
|
+
severity: "warn",
|
|
111
|
+
});
|
|
971
112
|
},
|
|
972
|
-
createRule,
|
|
973
113
|
};
|